diff --git a/.ci.yaml b/.ci.yaml index 2d0db05cd45f..57bfec26fd90 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -24,27 +24,37 @@ platform_properties: properties: dependencies: >- [ - {"dependency": "xcode", "version": "14c18"}, + {"dependency": "xcode", "version": "14e222b"}, {"dependency": "gems", "version": "v3.3.14"} ] os: Mac-12 device_type: none cpu: arm64 - xcode: 14c18 + xcode: 14e222b mac_x64: properties: dependencies: >- [ - {"dependency": "xcode", "version": "14c18"}, + {"dependency": "xcode", "version": "14e222b"}, {"dependency": "gems", "version": "v3.3.14"} ] os: Mac-12 device_type: none cpu: x86 - xcode: 14c18 + xcode: 14e222b targets: - ### iOS+macOS tasks *** + ### Linux tasks ### + - name: Linux repo_tools_tests + recipe: packages/packages + timeout: 30 + properties: + add_recipes_cq: "true" + target_file: repo_tools_tests.yaml + channel: master + version_file: flutter_master.version + + ### iOS+macOS tasks ### # TODO(stuartmorgan): Move this to ARM once google_maps_flutter has ARM # support. `pod lint` makes a synthetic target that doesn't respect the # pod's arch exclusions, so fails to build. @@ -139,12 +149,9 @@ targets: version_file: flutter_stable.version target_file: ios_build_all_packages.yaml - # TODO(stuartmorgan): Change all of the ios_platform_tests_* task timeouts - # to 60 minutes once https://github.com/flutter/flutter/issues/119750 is - # fixed. - name: Mac_arm64 ios_platform_tests_shard_1 master recipe: packages/packages - timeout: 120 + timeout: 60 properties: add_recipes_cq: "true" version_file: flutter_master.version @@ -153,7 +160,7 @@ targets: - name: Mac_arm64 ios_platform_tests_shard_2 master recipe: packages/packages - timeout: 120 + timeout: 60 properties: add_recipes_cq: "true" version_file: flutter_master.version @@ -162,7 +169,7 @@ targets: - name: Mac_arm64 ios_platform_tests_shard_3 master recipe: packages/packages - timeout: 120 + timeout: 60 properties: add_recipes_cq: "true" version_file: flutter_master.version @@ -171,7 +178,7 @@ targets: - name: Mac_arm64 ios_platform_tests_shard_4 master recipe: packages/packages - timeout: 120 + timeout: 60 properties: add_recipes_cq: "true" version_file: flutter_master.version @@ -180,7 +187,7 @@ targets: - name: Mac_arm64 ios_platform_tests_shard_5 master recipe: packages/packages - timeout: 120 + timeout: 60 properties: add_recipes_cq: "true" version_file: flutter_master.version @@ -191,7 +198,7 @@ targets: - name: Mac_arm64 ios_platform_tests_shard_1 stable recipe: packages/packages presubmit: false - timeout: 120 + timeout: 60 properties: channel: stable add_recipes_cq: "true" @@ -202,7 +209,7 @@ targets: - name: Mac_arm64 ios_platform_tests_shard_2 stable recipe: packages/packages presubmit: false - timeout: 120 + timeout: 60 properties: channel: stable add_recipes_cq: "true" @@ -213,7 +220,7 @@ targets: - name: Mac_arm64 ios_platform_tests_shard_3 stable recipe: packages/packages presubmit: false - timeout: 120 + timeout: 60 properties: channel: stable add_recipes_cq: "true" @@ -224,7 +231,7 @@ targets: - name: Mac_arm64 ios_platform_tests_shard_4 stable recipe: packages/packages presubmit: false - timeout: 120 + timeout: 60 properties: channel: stable add_recipes_cq: "true" @@ -235,7 +242,7 @@ targets: - name: Mac_arm64 ios_platform_tests_shard_5 stable recipe: packages/packages presubmit: false - timeout: 120 + timeout: 60 properties: channel: stable add_recipes_cq: "true" diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 224b9d72de9e..d1ddf61c9218 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -23,7 +23,7 @@ RUN apt-get install -y clang-format # - build tools. RUN apt-get install -y clang cmake ninja-build file pkg-config # - libraries. -RUN apt-get install -y libgtk-3-dev libblkid-dev liblzma-dev libgcrypt20-dev +RUN apt-get install -y libgtk-3-dev # - xvfb to allow running headless. RUN apt-get install -y xvfb libegl1-mesa diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 7ad6ea501624..b837ee2e4915 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -66fa4c5d301c5093cb4403b39e3d61f604b96d90 +343718945bcbcc356dbb8b1c51fcd5655dd5bb87 diff --git a/.ci/flutter_stable.version b/.ci/flutter_stable.version index f88f99fd4646..4e463678673b 100644 --- a/.ci/flutter_stable.version +++ b/.ci/flutter_stable.version @@ -1 +1 @@ -f72efea43c3013323d1b95cff571f3c1caa37583 +d3d8effc686d73e0114d71abdcccef63fa1f25d2 diff --git a/.ci/legacy_project/README.md b/.ci/legacy_project/README.md new file mode 100644 index 000000000000..c10ace0b2c2d --- /dev/null +++ b/.ci/legacy_project/README.md @@ -0,0 +1,37 @@ +This directory contains a partial snapshot of an old Flutter project; it is +intended to replace the corresponding parts of a newly Flutter-created project +to allow testing plugin builds with a legacy project. + +It was originally created with Flutter 2.0.6. In general the guidelines are: +- Pieces here should be largely self-contained rather than portions of + major project components; for instance, it currently contains the entire + `android/` directory from a legacy project, rather than a subset of it + which would be combined with a subset of a new project's `android/` + directory. This is to avoid random breakage in the future due to + conflicts between those subsets. For instance, we could probably get + away with not including android/app/src/main/res for a while, and + instead layer in the versions from a new project, but then someday + if the resources were renamed, there would be dangling references to + the old resources in files that are included here. +- Updates over time should be minimal. We don't expect that an unchanged + project will keep working forever, but this directory should simulate + a developer who has done the bare minimum to keep their project working + as they have updated Flutter. +- Updates should be logged below. + +The reason for the hybrid model, rather than checking in a full legacy +project, is to minimize unnecessary maintenance work. E.g., there's no +need to manually keep Dart code updated for Flutter changes just to +test legacy native Android build behaviors. + +## Manual changes to files + +The following are the changes relative to running: + +```bash +flutter create -a java all_packages +``` + +and then deleting everything but `android/` from it: + +- Added license boilerplate. diff --git a/.ci/legacy_project/all_packages/.gitignore b/.ci/legacy_project/all_packages/.gitignore new file mode 100644 index 000000000000..0fa6b675c0a5 --- /dev/null +++ b/.ci/legacy_project/all_packages/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# 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/ + +# Web related +lib/generated_plugin_registrant.dart + +# 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/.ci/legacy_project/all_packages/.metadata b/.ci/legacy_project/all_packages/.metadata new file mode 100644 index 000000000000..d7e64d0b3225 --- /dev/null +++ b/.ci/legacy_project/all_packages/.metadata @@ -0,0 +1,10 @@ +# 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 and should not be manually edited. + +version: + revision: 1d9032c7e1d867f071f2277eb1673e8f9b0274e3 + channel: unknown + +project_type: app diff --git a/.ci/legacy_project/all_packages/android/.gitignore b/.ci/legacy_project/all_packages/android/.gitignore new file mode 100644 index 000000000000..0a741cb43d66 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/.gitignore @@ -0,0 +1,11 @@ +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 diff --git a/.ci/legacy_project/all_packages/android/app/build.gradle b/.ci/legacy_project/all_packages/android/app/build.gradle new file mode 100644 index 000000000000..b75c7b0561bc --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/build.gradle @@ -0,0 +1,47 @@ +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 from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 30 + + defaultConfig { + applicationId "com.example.all_packages" + minSdkVersion 16 + targetSdkVersion 30 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} diff --git a/.ci/legacy_project/all_packages/android/app/src/debug/AndroidManifest.xml b/.ci/legacy_project/all_packages/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..3a38eba348d8 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/.ci/legacy_project/all_packages/android/app/src/main/AndroidManifest.xml b/.ci/legacy_project/all_packages/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..70c010f2867e --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/.ci/legacy_project/all_packages/android/app/src/main/java/com/example/all_packages/MainActivity.java b/.ci/legacy_project/all_packages/android/app/src/main/java/com/example/all_packages/MainActivity.java new file mode 100644 index 000000000000..f494afad857c --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/main/java/com/example/all_packages/MainActivity.java @@ -0,0 +1,10 @@ +// 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 com.example.all_packages; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity { +} diff --git a/.ci/legacy_project/all_packages/android/app/src/main/res/drawable-v21/launch_background.xml b/.ci/legacy_project/all_packages/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000000..f74085f3f6a2 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/.ci/legacy_project/all_packages/android/app/src/main/res/drawable/launch_background.xml b/.ci/legacy_project/all_packages/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/.ci/legacy_project/all_packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/.ci/legacy_project/all_packages/android/app/src/main/res/values-night/styles.xml b/.ci/legacy_project/all_packages/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000000..449a9f930826 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/.ci/legacy_project/all_packages/android/app/src/main/res/values/styles.xml b/.ci/legacy_project/all_packages/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..d74aa35c2826 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/.ci/legacy_project/all_packages/android/app/src/profile/AndroidManifest.xml b/.ci/legacy_project/all_packages/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000000..02ba522d3d9e --- /dev/null +++ b/.ci/legacy_project/all_packages/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/.ci/legacy_project/all_packages/android/build.gradle b/.ci/legacy_project/all_packages/android/build.gradle new file mode 100644 index 000000000000..c9e3db0a0f33 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/.ci/legacy_project/all_packages/android/gradle.properties b/.ci/legacy_project/all_packages/android/gradle.properties new file mode 100644 index 000000000000..94adc3a3f97a --- /dev/null +++ b/.ci/legacy_project/all_packages/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/.ci/legacy_project/all_packages/android/gradle/wrapper/gradle-wrapper.properties b/.ci/legacy_project/all_packages/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..bc6a58afdda2 --- /dev/null +++ b/.ci/legacy_project/all_packages/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +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 diff --git a/.ci/legacy_project/all_packages/android/settings.gradle b/.ci/legacy_project/all_packages/android/settings.gradle new file mode 100644 index 000000000000..44e62bcf06ae --- /dev/null +++ b/.ci/legacy_project/all_packages/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/.ci/scripts/build_all_packages_app.sh b/.ci/scripts/build_all_packages_app.sh index c22b9832ff22..7b381acc4e9c 100755 --- a/.ci/scripts/build_all_packages_app.sh +++ b/.ci/scripts/build_all_packages_app.sh @@ -2,6 +2,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. +set -e platform="$1" build_mode="$2" diff --git a/.ci/scripts/build_examples_win32.sh b/.ci/scripts/build_examples_win32.sh index bcf57a4b311f..8d87a2e660e6 100755 --- a/.ci/scripts/build_examples_win32.sh +++ b/.ci/scripts/build_examples_win32.sh @@ -2,6 +2,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. +set -e dart ./script/tool/bin/flutter_plugin_tools.dart build-examples --windows \ --packages-for-branch --log-timing diff --git a/.ci/scripts/create_all_packages_app.sh b/.ci/scripts/create_all_packages_app.sh index 8399e5e38a35..4440dfe2608a 100755 --- a/.ci/scripts/create_all_packages_app.sh +++ b/.ci/scripts/create_all_packages_app.sh @@ -2,6 +2,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. +set -e dart ./script/tool/bin/flutter_plugin_tools.dart create-all-packages-app \ --output-dir=. --exclude script/configs/exclude_all_packages_app.yaml diff --git a/.ci/scripts/create_simulator.sh b/.ci/scripts/create_simulator.sh index a12bbee0af0c..04d8fd1e9cd5 100755 --- a/.ci/scripts/create_simulator.sh +++ b/.ci/scripts/create_simulator.sh @@ -2,11 +2,14 @@ # 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. +set -e +# Ensure that the create/boot pipeline fails if `create` fails +set -o pipefail # The name here must match remove_simulator.sh readonly DEVICE_NAME=Flutter-iPhone readonly DEVICE=com.apple.CoreSimulator.SimDeviceType.iPhone-14 -readonly OS=com.apple.CoreSimulator.SimRuntime.iOS-16-2 +readonly OS=com.apple.CoreSimulator.SimRuntime.iOS-16-4 xcrun simctl list xcrun simctl create "$DEVICE_NAME" "$DEVICE" "$OS" | xargs xcrun simctl boot diff --git a/.ci/scripts/custom_package_tests.sh b/.ci/scripts/custom_package_tests.sh index 6b37bfbf1a96..c6473b267dc3 100755 --- a/.ci/scripts/custom_package_tests.sh +++ b/.ci/scripts/custom_package_tests.sh @@ -2,6 +2,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. +set -e # Exclusions # diff --git a/.ci/scripts/dart_unit_tests_win32.sh b/.ci/scripts/dart_unit_tests_win32.sh index bd1ba77b5916..5fbe4764f6b3 100755 --- a/.ci/scripts/dart_unit_tests_win32.sh +++ b/.ci/scripts/dart_unit_tests_win32.sh @@ -2,6 +2,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. +set -e dart ./script/tool/bin/flutter_plugin_tools.dart test \ --exclude=script/configs/windows_unit_tests_exceptions.yaml \ diff --git a/.ci/scripts/drive_examples_win32.sh b/.ci/scripts/drive_examples_win32.sh index c3e2e7bc5447..671330179e2f 100755 --- a/.ci/scripts/drive_examples_win32.sh +++ b/.ci/scripts/drive_examples_win32.sh @@ -2,6 +2,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. +set -e dart ./script/tool/bin/flutter_plugin_tools.dart drive-examples --windows \ --exclude=script/configs/exclude_integration_win32.yaml --packages-for-branch --log-timing diff --git a/.ci/scripts/native_test_win32.sh b/.ci/scripts/native_test_win32.sh index 37cf54e55c5c..0c86d3601e62 100755 --- a/.ci/scripts/native_test_win32.sh +++ b/.ci/scripts/native_test_win32.sh @@ -2,6 +2,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. +set -e dart ./script/tool/bin/flutter_plugin_tools.dart native-test --windows \ --no-integration --packages-for-branch --log-timing diff --git a/.ci/scripts/plugin_tools_tests.sh b/.ci/scripts/plugin_tools_tests.sh index 96eec4349f08..574a5fe51e46 100755 --- a/.ci/scripts/plugin_tools_tests.sh +++ b/.ci/scripts/plugin_tools_tests.sh @@ -2,6 +2,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. +set -e cd script/tool dart pub run test diff --git a/.ci/scripts/prepare_tool.sh b/.ci/scripts/prepare_tool.sh index f93694bf1ff6..6c178170f501 100755 --- a/.ci/scripts/prepare_tool.sh +++ b/.ci/scripts/prepare_tool.sh @@ -2,6 +2,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. +set -e # To set FETCH_HEAD for "git merge-base" to work git fetch origin main diff --git a/.ci/scripts/remove_simulator.sh b/.ci/scripts/remove_simulator.sh index e15354e600b7..09203011706f 100755 --- a/.ci/scripts/remove_simulator.sh +++ b/.ci/scripts/remove_simulator.sh @@ -2,10 +2,12 @@ # 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. +set -e # The name here must match create_simulator.sh readonly DEVICE_NAME=Flutter-iPhone -xcrun simctl shutdown "$DEVICE_NAME" +# Allow shutdown to fail; cases like "already shut down" exit with failure. +xcrun simctl shutdown "$DEVICE_NAME" || : xcrun simctl delete "$DEVICE_NAME" xcrun simctl list diff --git a/.ci/targets/plugin_tools_tests.yaml b/.ci/targets/plugin_tools_tests.yaml deleted file mode 100644 index 265e74bdd06b..000000000000 --- a/.ci/targets/plugin_tools_tests.yaml +++ /dev/null @@ -1,5 +0,0 @@ -tasks: - - name: prepare tool - script: .ci/scripts/prepare_tool.sh - - name: tool unit tests - script: .ci/scripts/plugin_tools_tests.sh diff --git a/.ci/targets/repo_tools_tests.yaml b/.ci/targets/repo_tools_tests.yaml index a73053e34524..265e74bdd06b 100644 --- a/.ci/targets/repo_tools_tests.yaml +++ b/.ci/targets/repo_tools_tests.yaml @@ -1,4 +1,5 @@ tasks: - name: prepare tool script: .ci/scripts/prepare_tool.sh - # TODO(stuartmorgan): Add actual tests here when moving the repo tooling. + - name: tool unit tests + script: .ci/scripts/plugin_tools_tests.sh diff --git a/.cirrus.yml b/.cirrus.yml index 7d00bcbc75cc..c8a7af48cce9 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,4 +1,4 @@ -gcp_credentials: ENCRYPTED[!3a93d98d7c95a41f5033834ef30e50928fc5d81239dc632b153c2628200a8187f3811cb01ce338b1ab3b6505a7a65c37!] +gcp_credentials: ENCRYPTED[!9c8e92e8da192ce2a51b7d4ed9948c4250d0bae3660193d9b901196c9692abbebe784d4a32e9f38b328571d65f6e7aed!] # Run on PRs and main branch post submit only. Don't run tests when tagging. only_if: $CIRRUS_TAG == '' && ($CIRRUS_PR != '' || $CIRRUS_BRANCH == 'main') @@ -27,7 +27,7 @@ macos_template: &MACOS_TEMPLATE # PRs on macOS. use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' macos_instance: - image: ghcr.io/cirruslabs/macos-ventura-xcode:14 + image: ghcr.io/cirruslabs/macos-ventura-xcode:14.3 flutter_upgrade_template: &FLUTTER_UPGRADE_TEMPLATE upgrade_flutter_script: @@ -83,11 +83,6 @@ task: zone: us-central1-a namespace: default matrix: - ### Platform-agnostic tasks ### - - name: Linux plugin_tools_tests - script: - - cd script/tool - - dart pub run test # Repository rules and best-practice enforcement. # Only channel-agnostic tests should go here since it is only run once # (on Flutter master). @@ -97,7 +92,7 @@ task: license_script: $PLUGIN_TOOL_COMMAND license-check # The major and minor version here should match the lowest version # analyzed in legacy_version_analyze. - pubspec_script: ./script/tool_runner.sh pubspec-check --min-min-flutter-version=3.0.0 --allow-dependencies=script/configs/allowed_unpinned_deps.yaml --allow-pinned-dependencies=script/configs/allowed_pinned_deps.yaml + pubspec_script: ./script/tool_runner.sh pubspec-check --min-min-flutter-version=3.3.0 --allow-dependencies=script/configs/allowed_unpinned_deps.yaml --allow-pinned-dependencies=script/configs/allowed_pinned_deps.yaml readme_script: - ./script/tool_runner.sh readme-check # Re-run with --require-excerpts, skipping packages that still need @@ -163,7 +158,7 @@ task: matrix: # Change the arguments to pubspec-check when changing these values. env: - CHANNEL: "3.0.5" + CHANNEL: "3.7.12" DART_VERSION: "2.17.6" env: CHANNEL: "3.3.10" @@ -265,18 +260,19 @@ task: skip: $CIRRUS_PR != '' && $CHANNEL == 'stable' env: matrix: - PACKAGE_SHARDING: "--shardIndex 0 --shardCount 7" - PACKAGE_SHARDING: "--shardIndex 1 --shardCount 7" - PACKAGE_SHARDING: "--shardIndex 2 --shardCount 7" - PACKAGE_SHARDING: "--shardIndex 3 --shardCount 7" - PACKAGE_SHARDING: "--shardIndex 4 --shardCount 7" - PACKAGE_SHARDING: "--shardIndex 5 --shardCount 7" - PACKAGE_SHARDING: "--shardIndex 6 --shardCount 7" + PACKAGE_SHARDING: "--shardIndex 0 --shardCount 8" + PACKAGE_SHARDING: "--shardIndex 1 --shardCount 8" + PACKAGE_SHARDING: "--shardIndex 2 --shardCount 8" + PACKAGE_SHARDING: "--shardIndex 3 --shardCount 8" + PACKAGE_SHARDING: "--shardIndex 4 --shardCount 8" + PACKAGE_SHARDING: "--shardIndex 5 --shardCount 8" + PACKAGE_SHARDING: "--shardIndex 6 --shardCount 8" + PACKAGE_SHARDING: "--shardIndex 7 --shardCount 8" matrix: CHANNEL: "master" CHANNEL: "stable" MAPS_API_KEY: ENCRYPTED[d6583b08f79f91ea4844c77460f04539965e46ad2fd97fb7c062b4dfe88016228b86ebe8c220ab4187e0c4bd773dc1e7] - GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[62dec6b6b5e7f32dd915552cc33202a350796824de621870bbc54c9ae28a10751e6bd8112f9e21f7f77e65fde1d588cf] + GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[1a2eebf9367197bbe812d9a0ea83a53a05aeba4bb5e4964fe6a69727883cd87e51238d39237b1f80b0894c48419ac268] build_script: - ./script/tool_runner.sh build-examples --apk lint_script: @@ -306,6 +302,11 @@ task: CHANNEL: "master" CHANNEL: "stable" << : *BUILD_ALL_PACKAGES_APP_TEMPLATE + create_all_packages_app_legacy_script: + - $PLUGIN_TOOL_COMMAND create-all-packages-app --legacy-source=.ci/legacy_project/all_packages --output-dir=legacy/ --exclude script/configs/exclude_all_packages_app.yaml + build_all_packages_legacy_script: + - cd legacy/all_packages + - flutter build $BUILD_ALL_ARGS --debug ### Web tasks ### - name: web-platform_tests env: @@ -350,7 +351,7 @@ task: # currently have a hard-coded device. create_simulator_script: - xcrun simctl list - - xcrun simctl create "iPhone 13" com.apple.CoreSimulator.SimDeviceType.iPhone-13 com.apple.CoreSimulator.SimRuntime.iOS-16-0 + - xcrun simctl create "iPhone 13" com.apple.CoreSimulator.SimDeviceType.iPhone-13 com.apple.CoreSimulator.SimRuntime.iOS-16-4 local_tests_script: # script/configs/linux_only_custom_test.yaml # Custom tests need Chrome for these packages. (They run in linux-custom_package_tests) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8ac6d0e71b3f..9eb0b6fb7b03 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,9 +20,18 @@ jobs: # Github Actions don't support templates so it is hard to share this snippet with another action # If we eventually need to use this in more workflow, we could create a shell script that contains this # snippet. + # + # This uses a pinned version of Flutter rather than `stable` so that it is + # not subject to out-of-band failures when new releases happen. It does + # not use the auto-rolled pin because there's no way for the autoroller + # to test the actual release flow, so changes would still show up in + # post-submit. A manually-rolled pin means that any changes here must be + # made deliberately, so that the person updating it knows to watch the + # next actual auto-release to ensure that it works, and knows to revert + # the change if it doesn't. run: | cd $HOME - git clone https://github.com/flutter/flutter.git --depth 1 -b stable _flutter + git clone https://github.com/flutter/flutter.git --depth 1 -b 3.10.0 _flutter echo "$HOME/_flutter/bin" >> $GITHUB_PATH cd $GITHUB_WORKSPACE # Checks out a copy of the repo. diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index ecccd64b43b5..d6b00ef80ff8 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@f3feb00acb00f31a6f60280e6ace9ca31d91c76a # v1.0.26 + uses: github/codeql-action/upload-sarif@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v1.0.26 with: sarif_file: results.sarif diff --git a/analysis_options.yaml b/analysis_options.yaml index 498d19dfb4ae..3c924bb88a30 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -13,8 +13,6 @@ analyzer: # allow self-reference to deprecated members (we do this because otherwise we have # to annotate every member in every test, assert, etc, when we deprecate something) deprecated_member_use_from_same_package: ignore - # Turned off until null-safe rollout is complete. - unnecessary_null_comparison: ignore exclude: # DIFFERENT FROM FLUTTER/FLUTTER # Ignore generated files - '**/*.g.dart' diff --git a/packages/animations/CHANGELOG.md b/packages/animations/CHANGELOG.md index a6733e2416c5..65cbb350dbf7 100644 --- a/packages/animations/CHANGELOG.md +++ b/packages/animations/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. * Aligns Dart and Flutter SDK constraints. ## 2.0.7 diff --git a/packages/animations/example/android/.pluginToolsConfig.yaml b/packages/animations/example/android/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/animations/example/android/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/animations/example/android/build.gradle b/packages/animations/example/android/build.gradle index 4b30292ebe1f..ce647a433bd0 100644 --- a/packages/animations/example/android/build.gradle +++ b/packages/animations/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.21' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() diff --git a/packages/animations/example/pubspec.yaml b/packages/animations/example/pubspec.yaml index 5339c3ca0628..6a1c151615b8 100644 --- a/packages/animations/example/pubspec.yaml +++ b/packages/animations/example/pubspec.yaml @@ -6,8 +6,8 @@ publish_to: none version: 0.0.1 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: animations: diff --git a/packages/animations/pubspec.yaml b/packages/animations/pubspec.yaml index 2633ec49488f..559bd2d46271 100644 --- a/packages/animations/pubspec.yaml +++ b/packages/animations/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.0.7 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 1a783b163915..856f2bac5b31 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.10.5+1 + +* Removes obsolete null checks on non-nullable values. + +## 0.10.5 + +* Adds NV21 as an image streaming option for Android. + ## 0.10.4 * Allows camera to be switched while video recording. diff --git a/packages/camera/camera/example/.pluginToolsConfig.yaml b/packages/camera/camera/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/camera/camera/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 73c9f052d48c..6dad1ebe7daa 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -250,9 +250,7 @@ class _CameraExampleHomeState extends State child: Center( child: AspectRatio( aspectRatio: - localVideoController.value.size != null - ? localVideoController.value.aspectRatio - : 1.0, + localVideoController.value.aspectRatio, child: VideoPlayer(localVideoController)), ), ), @@ -1000,7 +998,7 @@ class _CameraExampleHomeState extends State : VideoPlayerController.file(File(videoFile!.path)); videoPlayerListener = () { - if (videoController != null && videoController!.value.size != null) { + if (videoController != null) { // Refreshing the state to update video player with the correct ratio. if (mounted) { setState(() {}); diff --git a/packages/camera/camera/lib/src/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart index bfcad6626dd6..e1303f6d16e0 100644 --- a/packages/camera/camera/lib/src/camera_image.dart +++ b/packages/camera/camera/lib/src/camera_image.dart @@ -90,6 +90,9 @@ ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) { // android.graphics.ImageFormat.JPEG case 256: return ImageFormatGroup.jpeg; + // android.graphics.ImageFormat.NV21 + case 17: + return ImageFormatGroup.nv21; } } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 839f064bba12..5f18563836ff 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing Dart. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.10.4 +version: 0.10.5+1 environment: sdk: ">=2.18.0 <4.0.0" @@ -21,9 +21,9 @@ flutter: default_package: camera_web dependencies: - camera_android: ^0.10.5 + camera_android: ^0.10.7 camera_avfoundation: ^0.9.13 - camera_platform_interface: ^2.4.0 + camera_platform_interface: ^2.5.0 camera_web: ^0.3.1 flutter: sdk: flutter diff --git a/packages/camera/camera/test/camera_image_test.dart b/packages/camera/camera/test/camera_image_test.dart index ecf4b509e2e4..5a046518a51a 100644 --- a/packages/camera/camera/test/camera_image_test.dart +++ b/packages/camera/camera/test/camera_image_test.dart @@ -139,6 +139,30 @@ void main() { expect(cameraImage.format.group, ImageFormatGroup.yuv420); }); + test('$CameraImage has ImageFormatGroup.nv21 for android', () { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final CameraImage cameraImage = + CameraImage.fromPlatformData({ + '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); + }); + test('$CameraImage has ImageFormatGroup.bgra8888 for iOS', () { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; @@ -162,6 +186,7 @@ void main() { }); expect(cameraImage.format.group, ImageFormatGroup.bgra8888); }); + test('$CameraImage has ImageFormatGroup.unknown', () { final CameraImage cameraImage = CameraImage.fromPlatformData({ diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index 4adba0df5152..28552d33c3ab 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.10.8+2 + +* Removes obsolete null checks on non-nullable values. + +## 0.10.8+1 + +* Fixes lint errors. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 0.10.8 * Updates gradle, AGP and fixes some lint errors. diff --git a/packages/camera/camera_android/android/build.gradle b/packages/camera/camera_android/android/build.gradle index 836aeaf13670..ef8d9e297674 100644 --- a/packages/camera/camera_android/android/build.gradle +++ b/packages/camera/camera_android/android/build.gradle @@ -34,7 +34,6 @@ android { compileSdkVersion 33 defaultConfig { - targetSdkVersion 31 minSdkVersion 21 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -48,10 +47,6 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } - lint { - baseline = file("lint-baseline.xml") - } - testOptions { unitTests.includeAndroidResources = true unitTests.returnDefaultValues = true diff --git a/packages/camera/camera_android/android/lint-baseline.xml b/packages/camera/camera_android/android/lint-baseline.xml deleted file mode 100644 index 7bf52079ebc2..000000000000 --- a/packages/camera/camera_android/android/lint-baseline.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraDeviceWrapper.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraDeviceWrapper.java index 6a8a8f6bee0b..d86535c1b0a8 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraDeviceWrapper.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraDeviceWrapper.java @@ -24,7 +24,6 @@ interface CameraDeviceWrapper { @TargetApi(Build.VERSION_CODES.P) void createCaptureSession(SessionConfiguration config) throws CameraAccessException; - @TargetApi(Build.VERSION_CODES.LOLLIPOP) void createCaptureSession( @NonNull List outputs, @NonNull CameraCaptureSession.StateCallback callback, diff --git a/packages/camera/camera_android/example/.pluginToolsConfig.yaml b/packages/camera/camera_android/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/camera/camera_android/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/camera/camera_android/example/lib/main.dart b/packages/camera/camera_android/example/lib/main.dart index dd02be3d7ae6..5162d3a22cfb 100644 --- a/packages/camera/camera_android/example/lib/main.dart +++ b/packages/camera/camera_android/example/lib/main.dart @@ -253,9 +253,7 @@ class _CameraExampleHomeState extends State child: Center( child: AspectRatio( aspectRatio: - localVideoController.value.size != null - ? localVideoController.value.aspectRatio - : 1.0, + localVideoController.value.aspectRatio, child: VideoPlayer(localVideoController)), ), ), @@ -1008,7 +1006,7 @@ class _CameraExampleHomeState extends State : VideoPlayerController.file(File(videoFile!.path)); videoPlayerListener = () { - if (videoController != null && videoController!.value.size != null) { + if (videoController != null) { // Refreshing the state to update video player with the correct ratio. if (mounted) { setState(() {}); diff --git a/packages/camera/camera_android/example/pubspec.yaml b/packages/camera/camera_android/example/pubspec.yaml index 39f40663d100..bee460e92fa5 100644 --- a/packages/camera/camera_android/example/pubspec.yaml +++ b/packages/camera/camera_android/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the camera plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: camera_android: @@ -32,8 +32,3 @@ 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/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml index deef7ee6a7ce..9da632825dd4 100644 --- a/packages/camera/camera_android/pubspec.yaml +++ b/packages/camera/camera_android/pubspec.yaml @@ -3,11 +3,11 @@ 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.8 +version: 0.10.8+2 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 12dd55552974..16da75f45970 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,22 +1,26 @@ -## NEXT - -* Updates minimum Flutter version to 3.3. -* Creates camera_android_camerax plugin for development. -* Adds CameraInfo class and removes unnecessary code from plugin. -* Adds CameraSelector class. -* Adds ProcessCameraProvider class. -* Bump CameraX version to 1.3.0-alpha02. -* Adds Camera and UseCase classes, along with methods for binding UseCases to a lifecycle with the ProcessCameraProvider. -* Bump CameraX version to 1.3.0-alpha03 and Kotlin version to 1.8.0. -* Changes instance manager to allow the separate creation of identical objects. -* Adds Preview and Surface classes, along with other methods needed to implement camera preview. -* Adds implementation of availableCameras(). -* Implements camera preview, createCamera, initializeCamera, onCameraError, onDeviceOrientationChanged, and onCameraInitialized. -* Adds integration test to plugin. -* Bump CameraX version to 1.3.0-alpha04. -* Fixes instance manager hot restart behavior and fixes Java casting issue. -* 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. +## 0.5.0+4 + +* Removes obsolete null checks on non-nullable values. + +## 0.5.0+3 + +* Fixes Java lints. + +## 0.5.0+2 + +* Adds a dependency on kotlin-bom to align versions of Kotlin transitive dependencies. +* Removes note in `README.md` regarding duplicate Kotlin classes issue. + +## 0.5.0+1 + +* Update `README.md` to include known duplicate Kotlin classes issue. + +## 0.5.0 + +* Initial release of this `camera` implementation that supports: + * Image capture + * Video recording + * Displaying a live camera preview + * Image streaming + + See [`README.md`](README.md) for more details on the limitations of this implementation. diff --git a/packages/camera/camera_android_camerax/CONTRIBUTING.md b/packages/camera/camera_android_camerax/CONTRIBUTING.md new file mode 100644 index 000000000000..3d365d791e57 --- /dev/null +++ b/packages/camera/camera_android_camerax/CONTRIBUTING.md @@ -0,0 +1,75 @@ +# Contributing to camera\_android\_camerax + +## Plugin structure + +The `camera_platform_interface` implementation is located at +`lib/src/android_camera_camerax.dart`, and it is implemented using Dart classes +that are wrapped versions of native Android Java classes. + +In this approach, each native Android library used in the plugin implementation +is represented by an equivalent Dart class. Instances of these classes are +considered paired and represent each other in Java and Dart, respectively. An +`InstanceManager`, which is essentially a map between `long` identifiers and +objects that also provides notifications when an object has become unreachable +by memory. There is both a Dart and Java `InstanceManager` implementation, so +when a Dart instance is created that represens an Android native instance, +both are stored in the `InstanceManager` of their respective language with a +shared `long` identifier. These `InstanceManager`s take callbacks that run +when objects become unrechable or removed, allowing the Dart library to easily +handle removing references to native resources automatically. To ensure all +created instances are properly managed and to more easily allow for testing, +each wrapped Android native class in Dart takes an `InstanceManager` and has +a detached constructor, a constructor that allows for the creation of instances +not attached to the `InstanceManager` and unlinked to a paired Android native +instance. + +In `lib/src/`, you will find all of the Dart-wrapped native Android classes that +the plugin currently uses in its implementation. As aforementioned, each of +these classes uses an `InstanceManager` (implementation in `instance_manager.dart`) +to manage objects that are created by the plugin implementation that map to objects +of the same type created in the native Android code. This plugin uses [`pigeon`][1] +to generate the communication layer between Flutter and native Android code, so each +of these Dart-wrapped classes may also have Host API and Flutter API implementations +that handle communication to the host native Android platform and from the host +native Android platform, respectively. The communication interface is defined in +the `pigeons/camerax_library.dart` file. After editing the communication interface, +regenerate the communication layer by running +`dart run pigeon --input pigeons/camerax_library.dart` from the plugin root. + +In the native Java Android code in `android/src/main/java/io/flutter/plugins/camerax/`, +you'll find the Host API and Flutter API implementations of the same classes +wrapped with Dart in `lib/src/` that handle communication from that Dart code +and to that Dart code, respectively. The Host API implementations should directly +delegate calls to the CameraX or other wrapped Android libraries and should not +have any additional logic or abstraction; any exceptions should be thoroughly +documented in the code. As aforementioned, the objects created in the native +Android code map to objects created on the Dart side and are also managed by +an `InstanceManager` (implementation in `InstanceManager.java`). + +If CameraX or other Android classes that you need to access do not have a +duplicately named implementation in `lib/src/`, then follow the same structure +described above to add them. + +For more information, please see the [design document][2] or feel free +to ask any questions on the #hackers-ecosystem channel on [Discord][6]. For +more information on contributing packages in general, check out our +[contribution guide][3]. + +## Testing + +While none of the generated `pigeon` files are tested, all plugin impelementation and +wrapped native Android classes (Java & Dart) are tested. You can find the Java tests under +`android/src/test/java/io/flutter/plugins/camerax/` and the Dart tests under `test/`. To +run these tests, please see the instructions in the [running plugin tests guide][5]. + +Besides [`pigeon`][1], this plugin also uses [`mockito`][4] to generate mock objects for +testing purposes. To generate the mock objects, run +`dart run build_runner build --delete-conflicting-outputs`. + + +[1]: https://pub.dev/packages/pigeon +[2]: https://docs.google.com/document/d/1wXB1zNzYhd2SxCu1_BK3qmNWRhonTB6qdv4erdtBQqo/edit?usp=sharing&resourcekey=0-WOBqqOKiO9SARnziBg28pg +[3]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md +[4]: https://pub.dev/packages/mockito +[5]: https://github.com/flutter/flutter/wiki/Plugin-Tests#running-tests +[6]: https://github.com/flutter/flutter/wiki/Chat \ No newline at end of file diff --git a/packages/camera/camera_android_camerax/README.md b/packages/camera/camera_android_camerax/README.md index 06d837ac7214..a357008b8ba4 100644 --- a/packages/camera/camera_android_camerax/README.md +++ b/packages/camera/camera_android_camerax/README.md @@ -1,3 +1,68 @@ -# camera_android_camerax +# camera\_android\_camerax -An implementation of the camera plugin on Android using CameraX. +An Android implementation of [`camera`][1] that uses the [CameraX library][2]. + +*Note*: This package is under development, so please note the +[missing features and limitations](#missing-features-and-limitations), but +otherwise feel free to try out the current implementation and provide any +feedback by filing issues under [`flutter/flutter`][5] with `[camerax]` in +the title, which will be actively triaged. + +## Usage + +This package is [non-endorsed][3]; the endorsed Android implementation of `camera` +is [`camera_android`][4]. To use this implementation of the plugin instead of +`camera_android`, you will need to specify it in your `pubsepc.yaml` file as a +dependency in addition to `camera`: + +```yaml +dependencies: + # ...along with your other dependencies + camera: ^0.10.4 + camera_android_camerax: ^0.5.0 +``` + +## Missing features and limitations + +### Resolution configuration \[[Issue #120462][120462]\] + +Any specified `ResolutionPreset` wll go unused in favor of CameraX defaults and +`onCameraResolutionChanged` is unimplemented. + +### Locking/Unlocking capture orientation \[[Issue #125915][125915]\] + +`lockCaptureOrientation` & `unLockCaptureOrientation` are unimplemented. + +### Flash mode configuration \[[Issue #120715][120715]\] + +`setFlashMode` is unimplemented. + +### Exposure mode, point, & offset configuration \[[Issue #120468][120468]\] + +`setExposureMode`, `setExposurePoint`, & `setExposureOffset` are unimplemented. + +### Focus mode & point configuration \[[Issue #120467][120467]\] + +`setFocusMode` & `setFocusPoint` are unimplemented. + +### Zoom configuration \[[Issue #125371][125371]\] + +`setZoomLevel` is unimplemented. + +## Contributing + +For more information on contributing to this plugin, see [`CONTRIBUTING.md`](CONTRIBUTING.md). + + + +[1]: https://pub.dev/packages/camera +[2]: https://developer.android.com/training/camerax +[3]: https://docs.flutter.dev/packages-and-plugins/developing-packages#non-endorsed-federated-plugin +[4]: https://pub.dev/packages/camera_android +[5]: https://github.com/flutter/flutter/issues/new/choose +[120462]: https://github.com/flutter/flutter/issues/120462 +[125915]: https://github.com/flutter/flutter/issues/125915 +[120715]: https://github.com/flutter/flutter/issues/120715 +[120468]: https://github.com/flutter/flutter/issues/120468 +[120467]: https://github.com/flutter/flutter/issues/120467 +[125371]: https://github.com/flutter/flutter/issues/125371 \ No newline at end of file diff --git a/packages/camera/camera_android_camerax/android/build.gradle b/packages/camera/camera_android_camerax/android/build.gradle index d056449de871..77b10450914a 100644 --- a/packages/camera/camera_android_camerax/android/build.gradle +++ b/packages/camera/camera_android_camerax/android/build.gradle @@ -56,19 +56,23 @@ android { checkAllWarnings true warningsAsErrors true disable 'AndroidGradlePluginVersion', 'GradleDependency', 'InvalidPackage' - baseline file("lint-baseline.xml") } } dependencies { // CameraX core library using the camera2 implementation must use same version number. - def camerax_version = "1.3.0-alpha04" + def camerax_version = "1.3.0-alpha05" implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}" + implementation "androidx.camera:camera-video:${camerax_version}" implementation 'com.google.guava:guava:31.1-android' testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-inline:5.0.0' testImplementation 'androidx.test:core:1.4.0' testImplementation 'org.robolectric:robolectric:4.8' + + // 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.10")) } diff --git a/packages/camera/camera_android_camerax/android/lint-baseline.xml b/packages/camera/camera_android_camerax/android/lint-baseline.xml deleted file mode 100644 index 1142794741a6..000000000000 --- a/packages/camera/camera_android_camerax/android/lint-baseline.xml +++ /dev/null @@ -1,1434 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 31f996d26e24..dd8ab514652c 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 @@ -7,6 +7,7 @@ import android.app.Activity; import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.lifecycle.LifecycleOwner; import io.flutter.embedding.engine.plugins.FlutterPlugin; @@ -19,11 +20,17 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, ActivityAware { private InstanceManager instanceManager; private FlutterPluginBinding pluginBinding; + private PendingRecordingHostApiImpl pendingRecordingHostApiImpl; + private RecorderHostApiImpl recorderHostApiImpl; + private VideoCaptureHostApiImpl videoCaptureHostApiImpl; private ImageAnalysisHostApiImpl imageAnalysisHostApiImpl; private ImageCaptureHostApiImpl imageCaptureHostApiImpl; - public SystemServicesHostApiImpl systemServicesHostApiImpl; + public @Nullable SystemServicesHostApiImpl systemServicesHostApiImpl; - @VisibleForTesting ProcessCameraProviderHostApiImpl processCameraProviderHostApiImpl; + @VisibleForTesting + public @Nullable ProcessCameraProviderHostApiImpl processCameraProviderHostApiImpl; + + @VisibleForTesting public @Nullable LiveDataHostApiImpl liveDataHostApiImpl; /** * Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment. @@ -48,8 +55,10 @@ public void setUp( // Set up Host APIs. GeneratedCameraXLibrary.InstanceManagerHostApi.setup( binaryMessenger, () -> instanceManager.clear()); + GeneratedCameraXLibrary.CameraHostApi.setup( + binaryMessenger, new CameraHostApiImpl(binaryMessenger, instanceManager)); GeneratedCameraXLibrary.CameraInfoHostApi.setup( - binaryMessenger, new CameraInfoHostApiImpl(instanceManager)); + binaryMessenger, new CameraInfoHostApiImpl(binaryMessenger, instanceManager)); GeneratedCameraXLibrary.CameraSelectorHostApi.setup( binaryMessenger, new CameraSelectorHostApiImpl(binaryMessenger, instanceManager)); GeneratedCameraXLibrary.JavaObjectHostApi.setup( @@ -58,19 +67,36 @@ public void setUp( new ProcessCameraProviderHostApiImpl(binaryMessenger, instanceManager, context); GeneratedCameraXLibrary.ProcessCameraProviderHostApi.setup( binaryMessenger, processCameraProviderHostApiImpl); - systemServicesHostApiImpl = new SystemServicesHostApiImpl(binaryMessenger, instanceManager); + systemServicesHostApiImpl = + new SystemServicesHostApiImpl(binaryMessenger, instanceManager, context); GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApiImpl); GeneratedCameraXLibrary.PreviewHostApi.setup( binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry)); imageCaptureHostApiImpl = new ImageCaptureHostApiImpl(binaryMessenger, instanceManager, context); GeneratedCameraXLibrary.ImageCaptureHostApi.setup(binaryMessenger, imageCaptureHostApiImpl); + GeneratedCameraXLibrary.CameraHostApi.setup( + binaryMessenger, new CameraHostApiImpl(binaryMessenger, instanceManager)); + liveDataHostApiImpl = new LiveDataHostApiImpl(binaryMessenger, instanceManager); + GeneratedCameraXLibrary.LiveDataHostApi.setup(binaryMessenger, liveDataHostApiImpl); + GeneratedCameraXLibrary.ObserverHostApi.setup( + binaryMessenger, new ObserverHostApiImpl(binaryMessenger, instanceManager)); 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)); + GeneratedCameraXLibrary.RecordingHostApi.setup( + binaryMessenger, new RecordingHostApiImpl(binaryMessenger, instanceManager)); + recorderHostApiImpl = new RecorderHostApiImpl(binaryMessenger, instanceManager, context); + GeneratedCameraXLibrary.RecorderHostApi.setup(binaryMessenger, recorderHostApiImpl); + pendingRecordingHostApiImpl = + new PendingRecordingHostApiImpl(binaryMessenger, instanceManager, context); + GeneratedCameraXLibrary.PendingRecordingHostApi.setup( + binaryMessenger, pendingRecordingHostApiImpl); + videoCaptureHostApiImpl = new VideoCaptureHostApiImpl(binaryMessenger, instanceManager); + GeneratedCameraXLibrary.VideoCaptureHostApi.setup(binaryMessenger, videoCaptureHostApiImpl); } @Override @@ -89,19 +115,18 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { @Override public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) { - setUp( - pluginBinding.getBinaryMessenger(), - pluginBinding.getApplicationContext(), - pluginBinding.getTextureRegistry()); - updateContext(pluginBinding.getApplicationContext()); - Activity activity = activityPluginBinding.getActivity(); + setUp(pluginBinding.getBinaryMessenger(), activity, pluginBinding.getTextureRegistry()); + updateContext(activity); + if (activity instanceof LifecycleOwner) { processCameraProviderHostApiImpl.setLifecycleOwner((LifecycleOwner) activity); + liveDataHostApiImpl.setLifecycleOwner((LifecycleOwner) activity); } else { ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity); processCameraProviderHostApiImpl.setLifecycleOwner(proxyLifecycleProvider); + liveDataHostApiImpl.setLifecycleOwner(proxyLifecycleProvider); } systemServicesHostApiImpl.setActivity(activity); @@ -133,6 +158,15 @@ public void updateContext(@NonNull Context context) { if (processCameraProviderHostApiImpl != null) { processCameraProviderHostApiImpl.setContext(context); } + if (recorderHostApiImpl != null) { + recorderHostApiImpl.setContext(context); + } + if (pendingRecordingHostApiImpl != null) { + pendingRecordingHostApiImpl.setContext(context); + } + if (systemServicesHostApiImpl != null) { + systemServicesHostApiImpl.setContext(context); + } if (imageCaptureHostApiImpl != null) { imageCaptureHostApiImpl.setContext(context); } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraFlutterApiImpl.java index a03548399485..cfc40be1819a 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraFlutterApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraFlutterApiImpl.java @@ -4,14 +4,16 @@ package io.flutter.plugins.camerax; +import androidx.annotation.NonNull; import androidx.camera.core.Camera; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraFlutterApi; public class CameraFlutterApiImpl extends CameraFlutterApi { - private final InstanceManager instanceManager; + private final @NonNull InstanceManager instanceManager; - public CameraFlutterApiImpl(BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + public CameraFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { super(binaryMessenger); this.instanceManager = instanceManager; } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraHostApiImpl.java new file mode 100644 index 000000000000..98f5f74b959d --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraHostApiImpl.java @@ -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. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.camera.core.Camera; +import androidx.camera.core.CameraInfo; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraHostApi; +import java.util.Objects; + +public class CameraHostApiImpl implements CameraHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + + public CameraHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + } + + /** + * Retrieves the {@link CameraInfo} instance that contains information about the {@link Camera} + * instance with the specified identifier. + */ + @Override + @NonNull + public Long getCameraInfo(@NonNull Long identifier) { + Camera camera = (Camera) Objects.requireNonNull(instanceManager.getInstance(identifier)); + CameraInfo cameraInfo = camera.getCameraInfo(); + + if (!instanceManager.containsInstance(cameraInfo)) { + CameraInfoFlutterApiImpl cameraInfoFlutterApiImpl = + new CameraInfoFlutterApiImpl(binaryMessenger, instanceManager); + cameraInfoFlutterApiImpl.create(cameraInfo, reply -> {}); + } + return instanceManager.getIdentifierForStrongReference(cameraInfo); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoFlutterApiImpl.java index c538e420cc7e..8b0e1ff6b3e2 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoFlutterApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoFlutterApiImpl.java @@ -4,20 +4,26 @@ package io.flutter.plugins.camerax; +import androidx.annotation.NonNull; import androidx.camera.core.CameraInfo; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraInfoFlutterApi; public class CameraInfoFlutterApiImpl extends CameraInfoFlutterApi { - private final InstanceManager instanceManager; + private final @NonNull InstanceManager instanceManager; public CameraInfoFlutterApiImpl( - BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { super(binaryMessenger); this.instanceManager = instanceManager; } + /** + * Creates a {@link CameraInfo} instance in Dart. {@code reply} is not used so it can be empty. + */ void create(CameraInfo cameraInfo, Reply reply) { - create(instanceManager.addHostCreatedInstance(cameraInfo), reply); + if (!instanceManager.containsInstance(cameraInfo)) { + create(instanceManager.addHostCreatedInstance(cameraInfo), reply); + } } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoHostApiImpl.java index d960b7fff70a..83eb359cdb5e 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoHostApiImpl.java @@ -5,21 +5,91 @@ package io.flutter.plugins.camerax; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import androidx.camera.core.CameraInfo; +import androidx.camera.core.CameraState; +import androidx.camera.core.ExposureState; +import androidx.camera.core.ZoomState; +import androidx.lifecycle.LiveData; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraInfoHostApi; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.LiveDataSupportedType; import java.util.Objects; public class CameraInfoHostApiImpl implements CameraInfoHostApi { + private final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; - public CameraInfoHostApiImpl(InstanceManager instanceManager) { + @VisibleForTesting public @NonNull LiveDataFlutterApiWrapper liveDataFlutterApiWrapper; + + public CameraInfoHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; this.instanceManager = instanceManager; + this.liveDataFlutterApiWrapper = + new LiveDataFlutterApiWrapper(binaryMessenger, instanceManager); } + /** + * Retrieves the sensor rotation degrees of the {@link androidx.camera.core.Camera} that is + * represented by the {@link CameraInfo} with the specified identifier. + */ @Override + @NonNull public Long getSensorRotationDegrees(@NonNull Long identifier) { CameraInfo cameraInfo = (CameraInfo) Objects.requireNonNull(instanceManager.getInstance(identifier)); return Long.valueOf(cameraInfo.getSensorRotationDegrees()); } + + /** + * Retrieves the {@link LiveData} of the {@link CameraState} that is tied to the {@link + * androidx.camera.core.Camera} that is represented by the {@link CameraInfo} with the specified + * identifier. + */ + @Override + @NonNull + public Long getCameraState(@NonNull Long identifier) { + CameraInfo cameraInfo = + (CameraInfo) Objects.requireNonNull(instanceManager.getInstance(identifier)); + LiveData liveCameraState = cameraInfo.getCameraState(); + liveDataFlutterApiWrapper.create( + liveCameraState, LiveDataSupportedType.CAMERA_STATE, reply -> {}); + return instanceManager.getIdentifierForStrongReference(liveCameraState); + } + + /** + * Retrieves the {@link ExposureState} of the {@link CameraInfo} with the specified identifier. + */ + @Override + @NonNull + public Long getExposureState(@NonNull Long identifier) { + CameraInfo cameraInfo = + (CameraInfo) Objects.requireNonNull(instanceManager.getInstance(identifier)); + ExposureState exposureState = cameraInfo.getExposureState(); + + ExposureStateFlutterApiImpl exposureStateFlutterApiImpl = + new ExposureStateFlutterApiImpl(binaryMessenger, instanceManager); + exposureStateFlutterApiImpl.create(exposureState, result -> {}); + + return instanceManager.getIdentifierForStrongReference(exposureState); + } + + /** + * Retrieves the {@link LiveData} of the {@link ZoomState} of the {@link CameraInfo} with the + * specified identifier. + */ + @NonNull + @Override + public Long getZoomState(@NonNull Long identifier) { + CameraInfo cameraInfo = + (CameraInfo) Objects.requireNonNull(instanceManager.getInstance(identifier)); + LiveData zoomState = cameraInfo.getZoomState(); + + LiveDataFlutterApiWrapper liveDataFlutterApiWrapper = + new LiveDataFlutterApiWrapper(binaryMessenger, instanceManager); + liveDataFlutterApiWrapper.create(zoomState, LiveDataSupportedType.ZOOM_STATE, reply -> {}); + + return instanceManager.getIdentifierForStrongReference(zoomState); + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraPermissionsManager.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraPermissionsManager.java index 19b1ee569a9b..28093ec43711 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraPermissionsManager.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraPermissionsManager.java @@ -8,6 +8,7 @@ import android.Manifest.permission; import android.app.Activity; import android.content.pm.PackageManager; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; @@ -99,7 +100,8 @@ static final class CameraRequestPermissionsListener } @Override - public boolean onRequestPermissionsResult(int id, String[] permissions, int[] grantResults) { + public boolean onRequestPermissionsResult( + int id, @NonNull String[] permissions, @NonNull int[] grantResults) { if (alreadyCalled || id != CAMERA_REQUEST_ID) { return false; } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorFlutterApiImpl.java index 6ca3782d8b59..a0b30fa25417 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorFlutterApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorFlutterApiImpl.java @@ -4,6 +4,7 @@ package io.flutter.plugins.camerax; +import androidx.annotation.NonNull; import androidx.camera.core.CameraSelector; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraSelectorFlutterApi; @@ -12,7 +13,7 @@ public class CameraSelectorFlutterApiImpl extends CameraSelectorFlutterApi { private final InstanceManager instanceManager; public CameraSelectorFlutterApiImpl( - BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { super(binaryMessenger); this.instanceManager = instanceManager; } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorHostApiImpl.java index 8ceafbcecc7b..bbd747342def 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorHostApiImpl.java @@ -5,6 +5,7 @@ package io.flutter.plugins.camerax; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.camera.core.CameraInfo; import androidx.camera.core.CameraSelector; @@ -18,16 +19,16 @@ public class CameraSelectorHostApiImpl implements CameraSelectorHostApi { private final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; - @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy(); + @VisibleForTesting public @NonNull CameraXProxy cameraXProxy = new CameraXProxy(); public CameraSelectorHostApiImpl( - BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { this.binaryMessenger = binaryMessenger; this.instanceManager = instanceManager; } @Override - public void create(@NonNull Long identifier, Long lensFacing) { + public void create(@NonNull Long identifier, @Nullable Long lensFacing) { CameraSelector.Builder cameraSelectorBuilder = cameraXProxy.createCameraSelectorBuilder(); if (lensFacing != null) { cameraSelectorBuilder = cameraSelectorBuilder.requireLensFacing(lensFacing.intValue()); @@ -36,10 +37,10 @@ public void create(@NonNull Long identifier, Long lensFacing) { } @Override - public List filter(@NonNull Long identifier, @NonNull List cameraInfoIds) { + public @NonNull List filter(@NonNull Long identifier, @NonNull List cameraInfoIds) { CameraSelector cameraSelector = (CameraSelector) Objects.requireNonNull(instanceManager.getInstance(identifier)); - List cameraInfosForFilter = new ArrayList(); + List cameraInfosForFilter = new ArrayList<>(); for (Number cameraInfoAsNumber : cameraInfoIds) { Long cameraInfoId = cameraInfoAsNumber.longValue(); @@ -50,7 +51,7 @@ public List filter(@NonNull Long identifier, @NonNull List cameraInf } List filteredCameraInfos = cameraSelector.filter(cameraInfosForFilter); - List filteredCameraInfosIds = new ArrayList(); + List filteredCameraInfosIds = new ArrayList<>(); for (CameraInfo cameraInfo : filteredCameraInfos) { Long filteredCameraInfoId = instanceManager.getIdentifierForStrongReference(cameraInfo); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraStateErrorFlutterApiWrapper.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraStateErrorFlutterApiWrapper.java new file mode 100644 index 000000000000..f4c8d6ee4afd --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraStateErrorFlutterApiWrapper.java @@ -0,0 +1,57 @@ +// 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.CameraState; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraStateErrorFlutterApi; + +/** + * Flutter API implementation for {@link CameraStateError}. + * + *

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 CameraStateErrorFlutterApiWrapper { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private CameraStateErrorFlutterApi cameraStateErrorFlutterApi; + + /** + * Constructs a {@link CameraStateErrorFlutterApiWrapper}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public CameraStateErrorFlutterApiWrapper( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + cameraStateErrorFlutterApi = new CameraStateErrorFlutterApi(binaryMessenger); + } + + /** + * Stores the {@link CameraStateError} instance and notifies Dart to create and store a new {@link + * CameraStateError} instance that is attached to this one. If {@code instance} has already been + * added, this method does nothing. + */ + public void create( + @NonNull CameraState.StateError instance, + @NonNull Long code, + @NonNull CameraStateErrorFlutterApi.Reply callback) { + if (!instanceManager.containsInstance(instance)) { + cameraStateErrorFlutterApi.create( + instanceManager.addHostCreatedInstance(instance), code, callback); + } + } + + /** Sets the Flutter API used to send messages to Dart. */ + @VisibleForTesting + void setApi(@NonNull CameraStateErrorFlutterApi api) { + this.cameraStateErrorFlutterApi = api; + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraStateFlutterApiWrapper.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraStateFlutterApiWrapper.java new file mode 100644 index 000000000000..cb1c30ad424e --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraStateFlutterApiWrapper.java @@ -0,0 +1,102 @@ +// 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.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.camera.core.CameraState; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraStateFlutterApi; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraStateType; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraStateTypeData; + +/** + * Flutter API implementation for {@link CameraState}. + * + *

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 CameraStateFlutterApiWrapper { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private CameraStateFlutterApi cameraStateFlutterApi; + + /** + * Constructs a {@link CameraStateFlutterApiWrapper}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public CameraStateFlutterApiWrapper( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + cameraStateFlutterApi = new CameraStateFlutterApi(binaryMessenger); + } + + /** + * Stores the {@link CameraState} instance and notifies Dart to create and store a new {@link + * CameraState} instance that is attached to this one. If {@code instance} has already been added, + * this method does nothing. + */ + public void create( + @NonNull CameraState instance, + @NonNull CameraStateType type, + @Nullable CameraState.StateError error, + @NonNull CameraStateFlutterApi.Reply callback) { + if (instanceManager.containsInstance(instance)) { + return; + } + + if (error != null) { + // if there is a problem with the current camera state, we need to create a CameraStateError + // to send to the Dart side. + new CameraStateErrorFlutterApiWrapper(binaryMessenger, instanceManager) + .create(error, Long.valueOf(error.getCode()), reply -> {}); + } + + cameraStateFlutterApi.create( + instanceManager.addHostCreatedInstance(instance), + new CameraStateTypeData.Builder().setValue(type).build(), + instanceManager.getIdentifierForStrongReference(error), + callback); + } + + /** Converts CameraX CameraState.Type to CameraStateType that the Dart side understands. */ + @NonNull + public static CameraStateType getCameraStateType(@NonNull CameraState.Type type) { + CameraStateType cameraStateType = null; + switch (type) { + case CLOSED: + cameraStateType = CameraStateType.CLOSED; + break; + case CLOSING: + cameraStateType = CameraStateType.CLOSING; + break; + case OPEN: + cameraStateType = CameraStateType.OPEN; + break; + case OPENING: + cameraStateType = CameraStateType.OPENING; + break; + case PENDING_OPEN: + cameraStateType = CameraStateType.PENDING_OPEN; + break; + } + + if (cameraStateType == null) { + throw new IllegalArgumentException( + "The CameraState.Type passed to this method was not recognized."); + } + return cameraStateType; + } + + /** Sets the Flutter API used to send messages to Dart. */ + @VisibleForTesting + void setApi(@NonNull CameraStateFlutterApi api) { + this.cameraStateFlutterApi = api; + } +} 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 da025b11dac2..af7fdc36a721 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 @@ -13,6 +13,7 @@ import androidx.camera.core.ImageAnalysis; import androidx.camera.core.ImageCapture; import androidx.camera.core.Preview; +import androidx.camera.video.Recorder; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo; import java.io.File; @@ -23,45 +24,57 @@ 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) { + public static @NonNull Size sizeFromResolution(@NonNull ResolutionInfo resolutionInfo) { return new Size(resolutionInfo.getWidth().intValue(), resolutionInfo.getHeight().intValue()); } - public CameraSelector.Builder createCameraSelectorBuilder() { + public @NonNull CameraSelector.Builder createCameraSelectorBuilder() { return new CameraSelector.Builder(); } - public CameraPermissionsManager createCameraPermissionsManager() { + /** Creates an instance of {@link CameraPermissionsManager}. */ + public @NonNull CameraPermissionsManager createCameraPermissionsManager() { return new CameraPermissionsManager(); } - public DeviceOrientationManager createDeviceOrientationManager( + /** Creates an instance of the {@link DeviceOrientationManager}. */ + public @NonNull DeviceOrientationManager createDeviceOrientationManager( @NonNull Activity activity, @NonNull Boolean isFrontFacing, - @NonNull int sensorOrientation, + int sensorOrientation, @NonNull DeviceOrientationManager.DeviceOrientationChangeCallback callback) { return new DeviceOrientationManager(activity, isFrontFacing, sensorOrientation, callback); } - public Preview.Builder createPreviewBuilder() { + /** Creates a builder for an instance of the {@link Preview} use case. */ + public @NonNull Preview.Builder createPreviewBuilder() { return new Preview.Builder(); } - public Surface createSurface(@NonNull SurfaceTexture surfaceTexture) { + /** Creates a {@link Surface} instance from the specified {@link SurfaceTexture}. */ + public @NonNull Surface createSurface(@NonNull SurfaceTexture surfaceTexture) { return new Surface(surfaceTexture); } /** - * Creates an instance of the {@code SystemServicesFlutterApiImpl}. + * Creates an instance of the {@link SystemServicesFlutterApiImpl}. * *

Included in this class to utilize the callback methods it provides, e.g. {@code * onCameraError(String)}. */ - public SystemServicesFlutterApiImpl createSystemServicesFlutterApiImpl( + public @NonNull SystemServicesFlutterApiImpl createSystemServicesFlutterApiImpl( @NonNull BinaryMessenger binaryMessenger) { return new SystemServicesFlutterApiImpl(binaryMessenger); } + /** Creates an instance of {@link Recorder.Builder}. */ + @NonNull + public Recorder.Builder createRecorderBuilder() { + return new Recorder.Builder(); + } + + /** Creates a builder for an instance of the {@link ImageCapture} use case. */ + @NonNull public ImageCapture.Builder createImageCaptureBuilder() { return new ImageCapture.Builder(); } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java index ebcb86433f65..67cac560db4d 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/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; @@ -106,7 +107,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) { @@ -163,7 +164,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. @@ -194,7 +195,7 @@ public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { } /** @return the last received UI orientation. */ - public PlatformChannel.DeviceOrientation getLastUIOrientation() { + public @Nullable PlatformChannel.DeviceOrientation getLastUIOrientation() { return this.lastOrientation; } @@ -236,6 +237,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(); @@ -254,6 +257,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_camerax/android/src/main/java/io/flutter/plugins/camerax/ExposureStateFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ExposureStateFlutterApiImpl.java new file mode 100644 index 000000000000..1f31f603a8a4 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ExposureStateFlutterApiImpl.java @@ -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. + +package io.flutter.plugins.camerax; + +import android.util.Range; +import androidx.annotation.NonNull; +import androidx.camera.core.ExposureState; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ExposureCompensationRange; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ExposureStateFlutterApi; + +public class ExposureStateFlutterApiImpl extends ExposureStateFlutterApi { + private final InstanceManager instanceManager; + + public ExposureStateFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + /** + * Creates a {@link ExposureState} on the Dart side with its exposure compensation range that can + * be used to set the exposure compensation index and its exposure compensation step, the smallest + * step by which the exposure compensation can be changed. + */ + void create(@NonNull ExposureState exposureState, @NonNull Reply reply) { + if (instanceManager.containsInstance(exposureState)) { + return; + } + + final Range exposureCompensationRangeFromState = + exposureState.getExposureCompensationRange(); + ExposureCompensationRange exposureCompensationRange = + new ExposureCompensationRange.Builder() + .setMinCompensation(exposureCompensationRangeFromState.getLower().longValue()) + .setMaxCompensation(exposureCompensationRangeFromState.getUpper().longValue()) + .build(); + final Double exposureCompensationStep = + exposureState.getExposureCompensationStep().doubleValue(); + + create( + instanceManager.addHostCreatedInstance(exposureState), + exposureCompensationRange, + exposureCompensationStep, + reply); + } +} 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 31cea4add1f1..c6f627993772 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.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.camerax; @@ -57,6 +57,36 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { return errorList; } + /** + * The states the camera can be in. + * + *

See https://developer.android.com/reference/androidx/camera/core/CameraState.Type. + */ + public enum CameraStateType { + CLOSED(0), + CLOSING(1), + OPEN(2), + OPENING(3), + PENDING_OPEN(4); + + final int index; + + private CameraStateType(final int index) { + this.index = index; + } + } + + public enum LiveDataSupportedType { + CAMERA_STATE(0), + ZOOM_STATE(1); + + final int index; + + private LiveDataSupportedType(final int index) { + this.index = index; + } + } + /** Generated class from Pigeon that represents data sent in messages. */ public static final class ResolutionInfo { private @NonNull Long width; @@ -207,6 +237,187 @@ ArrayList toList() { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class CameraStateTypeData { + private @NonNull CameraStateType value; + + public @NonNull CameraStateType getValue() { + return value; + } + + public void setValue(@NonNull CameraStateType 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. */ + CameraStateTypeData() {} + + public static final class Builder { + + private @Nullable CameraStateType value; + + public @NonNull Builder setValue(@NonNull CameraStateType setterArg) { + this.value = setterArg; + return this; + } + + public @NonNull CameraStateTypeData build() { + CameraStateTypeData pigeonReturn = new CameraStateTypeData(); + pigeonReturn.setValue(value); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add(value == null ? null : value.index); + return toListResult; + } + + static @NonNull CameraStateTypeData fromList(@NonNull ArrayList list) { + CameraStateTypeData pigeonResult = new CameraStateTypeData(); + Object value = list.get(0); + pigeonResult.setValue(value == null ? null : CameraStateType.values()[(int) value]); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class LiveDataSupportedTypeData { + private @NonNull LiveDataSupportedType value; + + public @NonNull LiveDataSupportedType getValue() { + return value; + } + + public void setValue(@NonNull LiveDataSupportedType 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. */ + LiveDataSupportedTypeData() {} + + public static final class Builder { + + private @Nullable LiveDataSupportedType value; + + public @NonNull Builder setValue(@NonNull LiveDataSupportedType setterArg) { + this.value = setterArg; + return this; + } + + public @NonNull LiveDataSupportedTypeData build() { + LiveDataSupportedTypeData pigeonReturn = new LiveDataSupportedTypeData(); + pigeonReturn.setValue(value); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add(value == null ? null : value.index); + return toListResult; + } + + static @NonNull LiveDataSupportedTypeData fromList(@NonNull ArrayList list) { + LiveDataSupportedTypeData pigeonResult = new LiveDataSupportedTypeData(); + Object value = list.get(0); + pigeonResult.setValue(value == null ? null : LiveDataSupportedType.values()[(int) value]); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class ExposureCompensationRange { + private @NonNull Long minCompensation; + + public @NonNull Long getMinCompensation() { + return minCompensation; + } + + public void setMinCompensation(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"minCompensation\" is null."); + } + this.minCompensation = setterArg; + } + + private @NonNull Long maxCompensation; + + public @NonNull Long getMaxCompensation() { + return maxCompensation; + } + + public void setMaxCompensation(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"maxCompensation\" is null."); + } + this.maxCompensation = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + ExposureCompensationRange() {} + + public static final class Builder { + + private @Nullable Long minCompensation; + + public @NonNull Builder setMinCompensation(@NonNull Long setterArg) { + this.minCompensation = setterArg; + return this; + } + + private @Nullable Long maxCompensation; + + public @NonNull Builder setMaxCompensation(@NonNull Long setterArg) { + this.maxCompensation = setterArg; + return this; + } + + public @NonNull ExposureCompensationRange build() { + ExposureCompensationRange pigeonReturn = new ExposureCompensationRange(); + pigeonReturn.setMinCompensation(minCompensation); + pigeonReturn.setMaxCompensation(maxCompensation); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(minCompensation); + toListResult.add(maxCompensation); + return toListResult; + } + + static @NonNull ExposureCompensationRange fromList(@NonNull ArrayList list) { + ExposureCompensationRange pigeonResult = new ExposureCompensationRange(); + Object minCompensation = list.get(0); + pigeonResult.setMinCompensation( + (minCompensation == null) + ? null + : ((minCompensation instanceof Integer) + ? (Integer) minCompensation + : (Long) minCompensation)); + Object maxCompensation = list.get(1); + pigeonResult.setMaxCompensation( + (maxCompensation == null) + ? null + : ((maxCompensation instanceof Integer) + ? (Integer) maxCompensation + : (Long) maxCompensation)); + return pigeonResult; + } + } + public interface Result { @SuppressWarnings("UnknownNullness") void success(T result); @@ -326,6 +537,15 @@ public interface CameraInfoHostApi { @NonNull Long getSensorRotationDegrees(@NonNull Long identifier); + @NonNull + Long getCameraState(@NonNull Long identifier); + + @NonNull + Long getExposureState(@NonNull Long identifier); + + @NonNull + Long getZoomState(@NonNull Long identifier); + /** The codec used by CameraInfoHostApi. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); @@ -361,6 +581,82 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable CameraInfo channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CameraInfoHostApi.getCameraState", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + Long output = + api.getCameraState( + (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.CameraInfoHostApi.getExposureState", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + Long output = + api.getExposureState( + (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.CameraInfoHostApi.getZoomState", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + Long output = + api.getZoomState((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); + } + } } } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ @@ -723,6 +1019,44 @@ public void create(@NonNull Long identifierArg, @NonNull Reply callback) { channelReply -> callback.reply(null)); } } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface CameraHostApi { + + @NonNull + Long getCameraInfo(@NonNull Long identifier); + + /** The codec used by CameraHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** Sets up an instance of `CameraHostApi` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable CameraHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CameraHostApi.getCameraInfo", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + Long output = + api.getCameraInfo((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); + } + } + } + } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class CameraFlutterApi { private final @NonNull BinaryMessenger binaryMessenger; @@ -788,6 +1122,9 @@ void startListeningForDeviceOrientationChange( void stopListeningForDeviceOrientationChange(); + @NonNull + String getTempFilePath(@NonNull String prefix, @NonNull String suffix); + /** The codec used by SystemServicesHostApi. */ static @NonNull MessageCodec getCodec() { return SystemServicesHostApiCodec.INSTANCE; @@ -880,6 +1217,32 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String prefixArg = (String) args.get(0); + String suffixArg = (String) args.get(1); + try { + String output = api.getTempFilePath(prefixArg, suffixArg); + wrapped.add(0, output); + } 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. */ @@ -1082,20 +1445,486 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable PreviewHos } } } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface VideoCaptureHostApi { - private static class ImageCaptureHostApiCodec extends StandardMessageCodec { - public static final ImageCaptureHostApiCodec INSTANCE = new ImageCaptureHostApiCodec(); + @NonNull + Long withOutput(@NonNull Long videoOutputId); - private ImageCaptureHostApiCodec() {} + @NonNull + Long getOutput(@NonNull Long identifier); - @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); - } + /** The codec used by VideoCaptureHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `VideoCaptureHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable VideoCaptureHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.VideoCaptureHostApi.withOutput", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number videoOutputIdArg = (Number) args.get(0); + try { + Long output = + api.withOutput( + (videoOutputIdArg == null) ? null : videoOutputIdArg.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.VideoCaptureHostApi.getOutput", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + Long output = + api.getOutput((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); + } + } + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class VideoCaptureFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public VideoCaptureFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by VideoCaptureFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void create(@NonNull Long identifierArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.VideoCaptureFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(identifierArg)), + channelReply -> callback.reply(null)); + } + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface RecorderHostApi { + + void create(@NonNull Long identifier, @Nullable Long aspectRatio, @Nullable Long bitRate); + + @NonNull + Long getAspectRatio(@NonNull Long identifier); + + @NonNull + Long getTargetVideoEncodingBitRate(@NonNull Long identifier); + + @NonNull + Long prepareRecording(@NonNull Long identifier, @NonNull String path); + + /** The codec used by RecorderHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `RecorderHostApi` to handle messages through the `binaryMessenger`. + */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable RecorderHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecorderHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + Number aspectRatioArg = (Number) args.get(1); + Number bitRateArg = (Number) args.get(2); + try { + api.create( + (identifierArg == null) ? null : identifierArg.longValue(), + (aspectRatioArg == null) ? null : aspectRatioArg.longValue(), + (bitRateArg == null) ? null : bitRateArg.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.RecorderHostApi.getAspectRatio", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + Long output = + api.getAspectRatio( + (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.RecorderHostApi.getTargetVideoEncodingBitRate", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + Long output = + api.getTargetVideoEncodingBitRate( + (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.RecorderHostApi.prepareRecording", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + String pathArg = (String) args.get(1); + try { + Long output = + api.prepareRecording( + (identifierArg == null) ? null : identifierArg.longValue(), pathArg); + wrapped.add(0, output); + } 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 RecorderFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public RecorderFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by RecorderFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void create( + @NonNull Long identifierArg, + @Nullable Long aspectRatioArg, + @Nullable Long bitRateArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecorderFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg, aspectRatioArg, bitRateArg)), + channelReply -> callback.reply(null)); + } + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface PendingRecordingHostApi { + + @NonNull + Long start(@NonNull Long identifier); + + /** The codec used by PendingRecordingHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `PendingRecordingHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable PendingRecordingHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PendingRecordingHostApi.start", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + Long output = + api.start((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); + } + } + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class PendingRecordingFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public PendingRecordingFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by PendingRecordingFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void create(@NonNull Long identifierArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PendingRecordingFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(identifierArg)), + channelReply -> callback.reply(null)); + } + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface RecordingHostApi { + + void close(@NonNull Long identifier); + + void pause(@NonNull Long identifier); + + void resume(@NonNull Long identifier); + + void stop(@NonNull Long identifier); + + /** The codec used by RecordingHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `RecordingHostApi` to handle messages through the `binaryMessenger`. + */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable RecordingHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecordingHostApi.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); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecordingHostApi.pause", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + api.pause((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); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecordingHostApi.resume", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + api.resume((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); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecordingHostApi.stop", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + api.stop((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 RecordingFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public RecordingFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by RecordingFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void create(@NonNull Long identifierArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecordingFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(identifierArg)), + channelReply -> callback.reply(null)); + } + } + + private static class ImageCaptureHostApiCodec extends StandardMessageCodec { + public static final ImageCaptureHostApiCodec INSTANCE = new ImageCaptureHostApiCodec(); + + private ImageCaptureHostApiCodec() {} + + @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 @@ -1201,19 +2030,168 @@ public void success(String result) { reply.reply(wrapped); } - public void error(Throwable error) { - ArrayList wrappedError = wrapError(error); - reply.reply(wrappedError); - } - }; + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.takePicture( + (identifierArg == null) ? null : identifierArg.longValue(), resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class CameraStateFlutterApiCodec extends StandardMessageCodec { + public static final CameraStateFlutterApiCodec INSTANCE = new CameraStateFlutterApiCodec(); + + private CameraStateFlutterApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return CameraStateTypeData.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof CameraStateTypeData) { + stream.write(128); + writeValue(stream, ((CameraStateTypeData) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class CameraStateFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public CameraStateFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by CameraStateFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return CameraStateFlutterApiCodec.INSTANCE; + } + + public void create( + @NonNull Long identifierArg, + @NonNull CameraStateTypeData typeArg, + @Nullable Long errorIdentifierArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CameraStateFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg, typeArg, errorIdentifierArg)), + channelReply -> callback.reply(null)); + } + } + + private static class ExposureStateFlutterApiCodec extends StandardMessageCodec { + public static final ExposureStateFlutterApiCodec INSTANCE = new ExposureStateFlutterApiCodec(); + + private ExposureStateFlutterApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return ExposureCompensationRange.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof ExposureCompensationRange) { + stream.write(128); + writeValue(stream, ((ExposureCompensationRange) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class ExposureStateFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public ExposureStateFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by ExposureStateFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return ExposureStateFlutterApiCodec.INSTANCE; + } + + public void create( + @NonNull Long identifierArg, + @NonNull ExposureCompensationRange exposureCompensationRangeArg, + @NonNull Double exposureCompensationStepArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ExposureStateFlutterApi.create", getCodec()); + channel.send( + new ArrayList( + Arrays.asList( + identifierArg, exposureCompensationRangeArg, exposureCompensationStepArg)), + channelReply -> callback.reply(null)); + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class ZoomStateFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public ZoomStateFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } - api.takePicture( - (identifierArg == null) ? null : identifierArg.longValue(), resultCallback); - }); - } else { - channel.setMessageHandler(null); - } - } + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by ZoomStateFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void create( + @NonNull Long identifierArg, + @NonNull Double minZoomRatioArg, + @NonNull Double maxZoomRatioArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ZoomStateFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg, minZoomRatioArg, maxZoomRatioArg)), + channelReply -> callback.reply(null)); } } @@ -1379,6 +2357,281 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable AnalyzerHo } } } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface ObserverHostApi { + + void create(@NonNull Long identifier); + + /** The codec used by ObserverHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `ObserverHostApi` to handle messages through the `binaryMessenger`. + */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ObserverHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ObserverHostApi.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 ObserverFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public ObserverFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by ObserverFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void onChanged( + @NonNull Long identifierArg, + @NonNull Long valueIdentifierArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ObserverFlutterApi.onChanged", getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg, valueIdentifierArg)), + channelReply -> callback.reply(null)); + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class CameraStateErrorFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public CameraStateErrorFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by CameraStateErrorFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void create( + @NonNull Long identifierArg, @NonNull Long codeArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CameraStateErrorFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg, codeArg)), + channelReply -> callback.reply(null)); + } + } + + private static class LiveDataHostApiCodec extends StandardMessageCodec { + public static final LiveDataHostApiCodec INSTANCE = new LiveDataHostApiCodec(); + + private LiveDataHostApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return LiveDataSupportedTypeData.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof LiveDataSupportedTypeData) { + stream.write(128); + writeValue(stream, ((LiveDataSupportedTypeData) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface LiveDataHostApi { + + void observe(@NonNull Long identifier, @NonNull Long observerIdentifier); + + void removeObservers(@NonNull Long identifier); + + @Nullable + Long getValue(@NonNull Long identifier, @NonNull LiveDataSupportedTypeData type); + + /** The codec used by LiveDataHostApi. */ + static @NonNull MessageCodec getCodec() { + return LiveDataHostApiCodec.INSTANCE; + } + /** + * Sets up an instance of `LiveDataHostApi` to handle messages through the `binaryMessenger`. + */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable LiveDataHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.LiveDataHostApi.observe", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + Number observerIdentifierArg = (Number) args.get(1); + try { + api.observe( + (identifierArg == null) ? null : identifierArg.longValue(), + (observerIdentifierArg == null) ? null : observerIdentifierArg.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.LiveDataHostApi.removeObservers", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + api.removeObservers((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); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.LiveDataHostApi.getValue", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + LiveDataSupportedTypeData typeArg = (LiveDataSupportedTypeData) args.get(1); + try { + Long output = + api.getValue( + (identifierArg == null) ? null : identifierArg.longValue(), typeArg); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class LiveDataFlutterApiCodec extends StandardMessageCodec { + public static final LiveDataFlutterApiCodec INSTANCE = new LiveDataFlutterApiCodec(); + + private LiveDataFlutterApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return LiveDataSupportedTypeData.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof LiveDataSupportedTypeData) { + stream.write(128); + writeValue(stream, ((LiveDataSupportedTypeData) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class LiveDataFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public LiveDataFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by LiveDataFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return LiveDataFlutterApiCodec.INSTANCE; + } + + public void create( + @NonNull Long identifierArg, + @NonNull LiveDataSupportedTypeData typeArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.LiveDataFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg, typeArg)), + channelReply -> callback.reply(null)); + } + } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class AnalyzerFlutterApi { private final @NonNull BinaryMessenger binaryMessenger; 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 0c00ed63f2f8..f2e71aa3eeee 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 @@ -27,7 +27,7 @@ public class ImageCaptureHostApiImpl implements ImageCaptureHostApi { public static final String TEMPORARY_FILE_NAME = "CAP"; public static final String JPG_FILE_TYPE = ".jpg"; - @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy(); + @VisibleForTesting public @NonNull CameraXProxy cameraXProxy = new CameraXProxy(); public ImageCaptureHostApiImpl( @NonNull BinaryMessenger binaryMessenger, @@ -42,7 +42,7 @@ public ImageCaptureHostApiImpl( * Sets the context that the {@link ImageCapture} will use to find a location to save a captured * image. */ - public void setContext(Context context) { + public void setContext(@NonNull Context context) { this.context = context; } @@ -101,7 +101,7 @@ public void takePicture( /** Creates a callback used when saving a captured image. */ @VisibleForTesting - public ImageCapture.OnImageSavedCallback createOnImageSavedCallback( + public @NonNull ImageCapture.OnImageSavedCallback createOnImageSavedCallback( @NonNull File file, @NonNull GeneratedCameraXLibrary.Result result) { return new ImageCapture.OnImageSavedCallback() { @Override 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 788d54f668a0..97ab805370df 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 @@ -67,7 +67,7 @@ public interface FinalizationListener { * @return a new `InstanceManager`. */ @NonNull - public static InstanceManager create(FinalizationListener finalizationListener) { + public static InstanceManager create(@NonNull FinalizationListener finalizationListener) { return new InstanceManager(finalizationListener); } @@ -109,7 +109,7 @@ public T remove(long identifier) { * `null` if the manager doesn't contain the value. */ @Nullable - public Long getIdentifierForStrongReference(Object instance) { + public Long getIdentifierForStrongReference(@Nullable Object instance) { logWarningIfFinalizationListenerHasStopped(); final Long identifier = identifiers.get(instance); @@ -130,7 +130,7 @@ public Long getIdentifierForStrongReference(Object instance) { * @param identifier the identifier to be paired with instance. This value must be >= 0 and * unique. */ - public void addDartCreatedInstance(Object instance, long identifier) { + public void addDartCreatedInstance(@NonNull Object instance, long identifier) { logWarningIfFinalizationListenerHasStopped(); addInstance(instance, identifier); } @@ -141,7 +141,7 @@ public void addDartCreatedInstance(Object instance, long identifier) { * @param instance the instance to be stored. This must be unique to all other added instances. * @return the unique identifier (>= 0) stored with instance. */ - public long addHostCreatedInstance(Object instance) { + public long addHostCreatedInstance(@NonNull Object instance) { logWarningIfFinalizationListenerHasStopped(); if (containsInstance(instance)) { @@ -178,7 +178,7 @@ public T getInstance(long identifier) { * @param instance the instance whose presence in this manager is to be tested. * @return whether this manager contains the given `instance`. */ - public boolean containsInstance(Object instance) { + public boolean containsInstance(@Nullable Object instance) { logWarningIfFinalizationListenerHasStopped(); return identifiers.containsKey(instance); } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/JavaObjectHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/JavaObjectHostApiImpl.java index 5dc0ba7fc8ba..538d4542cf4b 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/JavaObjectHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/JavaObjectHostApiImpl.java @@ -22,7 +22,7 @@ public class JavaObjectHostApiImpl implements JavaObjectHostApi { * * @param instanceManager maintains instances stored to communicate with Dart objects */ - public JavaObjectHostApiImpl(InstanceManager instanceManager) { + public JavaObjectHostApiImpl(@NonNull InstanceManager instanceManager) { this.instanceManager = instanceManager; } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/LiveDataFlutterApiWrapper.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/LiveDataFlutterApiWrapper.java new file mode 100644 index 000000000000..35af27ce6cc3 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/LiveDataFlutterApiWrapper.java @@ -0,0 +1,61 @@ +// 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.lifecycle.LiveData; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.LiveDataFlutterApi; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.LiveDataSupportedType; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.LiveDataSupportedTypeData; + +/** + * Flutter API implementation for {@link LiveData}. + * + *

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 LiveDataFlutterApiWrapper { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private LiveDataFlutterApi liveDataFlutterApi; + + /** + * Constructs a {@link LiveDataFlutterApiWrapper}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public LiveDataFlutterApiWrapper( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + liveDataFlutterApi = new LiveDataFlutterApi(binaryMessenger); + } + + /** + * Stores the {@link LiveData} instance and notifies Dart to create and store a new {@link + * LiveData} instance that is attached to this one. If {@code instance} has already been added, + * this method does nothing. + */ + public void create( + @NonNull LiveData instance, + @NonNull LiveDataSupportedType type, + @NonNull LiveDataFlutterApi.Reply callback) { + if (!instanceManager.containsInstance(instance)) { + liveDataFlutterApi.create( + instanceManager.addHostCreatedInstance(instance), + new LiveDataSupportedTypeData.Builder().setValue(type).build(), + callback); + } + } + + /** Sets the Flutter API used to send messages to Dart. */ + @VisibleForTesting + void setApi(@NonNull LiveDataFlutterApi api) { + this.liveDataFlutterApi = api; + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/LiveDataHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/LiveDataHostApiImpl.java new file mode 100644 index 000000000000..3e9e595d4b0f --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/LiveDataHostApiImpl.java @@ -0,0 +1,107 @@ +// 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.Nullable; +import androidx.camera.core.CameraState; +import androidx.camera.core.ZoomState; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.LiveDataHostApi; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.LiveDataSupportedType; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.LiveDataSupportedTypeData; +import java.util.Objects; + +/** + * Host API implementation for {@link LiveData}. + * + *

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 LiveDataHostApiImpl implements LiveDataHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private LifecycleOwner lifecycleOwner; + + /** + * Constructs a {@link LiveDataHostApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public LiveDataHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + } + + /** Sets {@link LifecycleOwner} used to observe the camera state if so requested. */ + public void setLifecycleOwner(@NonNull LifecycleOwner lifecycleOwner) { + this.lifecycleOwner = lifecycleOwner; + } + + /** + * Adds an {@link Observer} with the specified identifier to the observers list of this instance + * within the lifespan of the {@link lifecycleOwner}. + */ + @Override + @SuppressWarnings("unchecked") + public void observe(@NonNull Long identifier, @NonNull Long observerIdentifier) { + getLiveDataInstance(identifier) + .observe( + lifecycleOwner, + Objects.requireNonNull(instanceManager.getInstance(observerIdentifier))); + } + + /** Removes all observers of this instance that are tied to the {@link lifecycleOwner}. */ + @Override + public void removeObservers(@NonNull Long identifier) { + getLiveDataInstance(identifier).removeObservers(lifecycleOwner); + } + + @Override + @Nullable + public Long getValue(@NonNull Long identifier, @NonNull LiveDataSupportedTypeData type) { + Object value = getLiveDataInstance(identifier).getValue(); + if (value == null) { + return null; + } + + LiveDataSupportedType valueType = type.getValue(); + switch (valueType) { + case CAMERA_STATE: + return createCameraState((CameraState) value); + case ZOOM_STATE: + return createZoomState((ZoomState) value); + default: + throw new IllegalArgumentException( + "The type of LiveData whose value was requested is not supported."); + } + } + + /** Creates a {@link CameraState} on the Dart side and returns its identifier. */ + private Long createCameraState(CameraState cameraState) { + new CameraStateFlutterApiWrapper(binaryMessenger, instanceManager) + .create( + cameraState, + CameraStateFlutterApiWrapper.getCameraStateType(cameraState.getType()), + cameraState.getError(), + reply -> {}); + return instanceManager.getIdentifierForStrongReference(cameraState); + } + + /** Creates a {@link ZoomState} on the Dart side and returns its identifiers. */ + private Long createZoomState(ZoomState zoomState) { + new ZoomStateFlutterApiImpl(binaryMessenger, instanceManager).create(zoomState, reply -> {}); + return instanceManager.getIdentifierForStrongReference(zoomState); + } + + /** Retrieves the {@link LiveData} instance that has the specified identifier. */ + private LiveData getLiveDataInstance(@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/ObserverFlutterApiWrapper.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ObserverFlutterApiWrapper.java new file mode 100644 index 000000000000..f514ddbf86df --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ObserverFlutterApiWrapper.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 android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.camera.core.CameraState; +import androidx.camera.core.ZoomState; +import androidx.lifecycle.Observer; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ObserverFlutterApi; +import java.util.Objects; + +/** + * Flutter API implementation for {@link Observer}. + * + *

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 ObserverFlutterApiWrapper { + + private static final String TAG = "ObserverFlutterApi"; + + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private ObserverFlutterApi observerFlutterApi; + + @VisibleForTesting @Nullable public CameraStateFlutterApiWrapper cameraStateFlutterApiWrapper; + @VisibleForTesting @Nullable public ZoomStateFlutterApiImpl zoomStateFlutterApiImpl; + + /** + * Constructs a {@link ObserverFlutterApiWrapper}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public ObserverFlutterApiWrapper( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + observerFlutterApi = new ObserverFlutterApi(binaryMessenger); + } + + /** + * Sends a message to Dart to call {@link Observer.onChanged} on the Dart object representing + * {@code instance}. + */ + public void onChanged( + @NonNull Observer instance, + @NonNull T value, + @NonNull ObserverFlutterApi.Reply callback) { + + // Cast value to type of data that is being observed and create it on the Dart side + // if supported by this plugin. + // + // The supported types can be found in GeneratedCameraXLibrary.java as the + // LiveDataSupportedType enum. To add a new type, please follow the instructions + // found in pigeons/camerax_library.dart in the documentation for LiveDataSupportedType. + if (value instanceof CameraState) { + createCameraState((CameraState) value); + } else if (value instanceof ZoomState) { + createZoomState((ZoomState) value); + } else { + throw new UnsupportedOperationException( + "The type of value that was observed is not handled by this plugin."); + } + + Long observerIdentifier = instanceManager.getIdentifierForStrongReference(instance); + if (observerIdentifier == null) { + Log.e( + TAG, + "The Observer that received a callback has been garbage collected. Please create a new instance to receive any further data changes."); + return; + } + + observerFlutterApi.onChanged( + Objects.requireNonNull(observerIdentifier), + instanceManager.getIdentifierForStrongReference(value), + callback); + } + + /** Creates a {@link CameraState} on the Dart side. */ + private void createCameraState(CameraState cameraState) { + if (cameraStateFlutterApiWrapper == null) { + cameraStateFlutterApiWrapper = + new CameraStateFlutterApiWrapper(binaryMessenger, instanceManager); + } + cameraStateFlutterApiWrapper.create( + cameraState, + CameraStateFlutterApiWrapper.getCameraStateType(cameraState.getType()), + cameraState.getError(), + reply -> {}); + } + + /** Creates a {@link ZoomState} on the Dart side. */ + private void createZoomState(ZoomState zoomState) { + if (zoomStateFlutterApiImpl == null) { + zoomStateFlutterApiImpl = new ZoomStateFlutterApiImpl(binaryMessenger, instanceManager); + } + zoomStateFlutterApiImpl.create(zoomState, reply -> {}); + } + + /** Sets the Flutter API used to send messages to Dart. */ + @VisibleForTesting + void setApi(@NonNull ObserverFlutterApi api) { + this.observerFlutterApi = api; + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ObserverHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ObserverHostApiImpl.java new file mode 100644 index 000000000000..333607e4165e --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ObserverHostApiImpl.java @@ -0,0 +1,102 @@ +// 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.lifecycle.Observer; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ObserverHostApi; +import java.util.Objects; + +/** + * Host API implementation for {@link Observer}. + * + *

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 ObserverHostApiImpl implements ObserverHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private final ObserverProxy observerProxy; + + /** Proxy for constructors and static method of {@link Observer}. */ + @VisibleForTesting + public static class ObserverProxy { + + /** Creates an instance of {@link Observer}. */ + @NonNull + public ObserverImpl create( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + return new ObserverImpl(binaryMessenger, instanceManager); + } + } + + /** Implementation of {@link Observer} that passes arguments of callback methods to Dart. */ + public static class ObserverImpl implements Observer { + private ObserverFlutterApiWrapper observerFlutterApiWrapper; + + /** + * Constructs an instance of {@link Observer} that passes arguments of callbacks methods to + * Dart. + */ + public ObserverImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + super(); + observerFlutterApiWrapper = new ObserverFlutterApiWrapper(binaryMessenger, instanceManager); + } + + /** Method called when the data in observance is changed to {@code value}. */ + @Override + public void onChanged(T value) { + observerFlutterApiWrapper.onChanged(this, value, reply -> {}); + } + + /** Flutter API used to send messages back to Dart. */ + @VisibleForTesting + void setApi(@NonNull ObserverFlutterApiWrapper api) { + this.observerFlutterApiWrapper = api; + } + } + + /** + * Constructs a {@link ObserverHostApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public ObserverHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this(binaryMessenger, instanceManager, new ObserverProxy()); + } + + /** + * Constructs a {@link ObserverHostApiImpl}. + * + * @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 Observer} + */ + @VisibleForTesting + ObserverHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, + @NonNull InstanceManager instanceManager, + @NonNull ObserverProxy observerProxy) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + this.observerProxy = observerProxy; + } + + /** Creates an {@link Observer} instance with the specified observer. */ + @Override + public void create(@NonNull Long identifier) { + instanceManager.addDartCreatedInstance( + observerProxy.create(binaryMessenger, instanceManager), identifier); + } + + private Observer getObserverInstance(@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/PendingRecordingFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingFlutterApiImpl.java new file mode 100644 index 000000000000..9b4f71080562 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingFlutterApiImpl.java @@ -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 io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.camera.video.PendingRecording; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.PendingRecordingFlutterApi; + +public class PendingRecordingFlutterApiImpl extends PendingRecordingFlutterApi { + private final InstanceManager instanceManager; + + public PendingRecordingFlutterApiImpl( + @Nullable BinaryMessenger binaryMessenger, @Nullable InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + void create(@NonNull PendingRecording pendingRecording, @Nullable Reply reply) { + create(instanceManager.addHostCreatedInstance(pendingRecording), reply); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java new file mode 100644 index 000000000000..70119fac5d41 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java @@ -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. + +package io.flutter.plugins.camerax; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.camera.video.PendingRecording; +import androidx.camera.video.Recording; +import androidx.camera.video.VideoRecordEvent; +import androidx.core.content.ContextCompat; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.PendingRecordingHostApi; +import java.util.Objects; +import java.util.concurrent.Executor; + +public class PendingRecordingHostApiImpl implements PendingRecordingHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private Context context; + + @VisibleForTesting @NonNull public CameraXProxy cameraXProxy = new CameraXProxy(); + + @VisibleForTesting SystemServicesFlutterApiImpl systemServicesFlutterApi; + + @VisibleForTesting RecordingFlutterApiImpl recordingFlutterApi; + + public PendingRecordingHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, + @NonNull InstanceManager instanceManager, + @Nullable Context context) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + this.context = context; + systemServicesFlutterApi = cameraXProxy.createSystemServicesFlutterApiImpl(binaryMessenger); + recordingFlutterApi = new RecordingFlutterApiImpl(binaryMessenger, instanceManager); + } + + /** Sets the context, which is used to get the {@link Executor} needed to start the recording. */ + public void setContext(@Nullable Context context) { + this.context = context; + } + + /** + * Starts the given {@link PendingRecording}, creating a new {@link Recording}. The recording is + * then added to the instance manager and we return the corresponding identifier. + * + * @param identifier An identifier corresponding to a PendingRecording. + */ + @NonNull + @Override + public Long start(@NonNull Long identifier) { + PendingRecording pendingRecording = getPendingRecordingFromInstanceId(identifier); + Recording recording = + pendingRecording.start(this.getExecutor(), event -> handleVideoRecordEvent(event)); + recordingFlutterApi.create(recording, reply -> {}); + return Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(recording)); + } + + @Nullable + @VisibleForTesting + public Executor getExecutor() { + return ContextCompat.getMainExecutor(context); + } + + /** + * Handles {@link VideoRecordEvent}s that come in during video recording. Sends any errors + * encountered using {@link SystemServicesFlutterApiImpl}. + */ + @VisibleForTesting + public void handleVideoRecordEvent(@NonNull VideoRecordEvent event) { + if (event instanceof VideoRecordEvent.Finalize) { + VideoRecordEvent.Finalize castedEvent = (VideoRecordEvent.Finalize) event; + if (castedEvent.hasError()) { + systemServicesFlutterApi.sendCameraError(castedEvent.getCause().toString(), reply -> {}); + } + } + } + + private PendingRecording getPendingRecordingFromInstanceId(Long instanceId) { + return (PendingRecording) Objects.requireNonNull(instanceManager.getInstance(instanceId)); + } +} 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 8491fa25fd42..6f93fd0f87cb 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 @@ -19,12 +19,12 @@ import java.util.concurrent.Executors; public class PreviewHostApiImpl implements PreviewHostApi { - private final BinaryMessenger binaryMessenger; + final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; private final TextureRegistry textureRegistry; - @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy(); - @VisibleForTesting public TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture; + @VisibleForTesting public @NonNull CameraXProxy cameraXProxy = new CameraXProxy(); + @VisibleForTesting public @Nullable TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture; public PreviewHostApiImpl( @NonNull BinaryMessenger binaryMessenger, @@ -57,7 +57,7 @@ public void create( * by a Flutter {@link TextureRegistry.SurfaceTextureEntry} used to build the {@link Preview}. */ @Override - public Long setSurfaceProvider(@NonNull Long identifier) { + public @NonNull Long setSurfaceProvider(@NonNull Long identifier) { Preview preview = (Preview) Objects.requireNonNull(instanceManager.getInstance(identifier)); flutterSurfaceTexture = textureRegistry.createSurfaceTexture(); SurfaceTexture surfaceTexture = flutterSurfaceTexture.surfaceTexture(); @@ -72,10 +72,11 @@ public Long setSurfaceProvider(@NonNull Long identifier) { * {@code Preview} that is backed by a Flutter {@link TextureRegistry.SurfaceTextureEntry}. */ @VisibleForTesting - public Preview.SurfaceProvider createSurfaceProvider(@NonNull SurfaceTexture surfaceTexture) { + public @NonNull Preview.SurfaceProvider createSurfaceProvider( + @NonNull SurfaceTexture surfaceTexture) { return new Preview.SurfaceProvider() { @Override - public void onSurfaceRequested(SurfaceRequest request) { + public void onSurfaceRequested(@NonNull SurfaceRequest request) { surfaceTexture.setDefaultBufferSize( request.getResolution().getWidth(), request.getResolution().getHeight()); Surface flutterSurface = cameraXProxy.createSurface(surfaceTexture); @@ -83,7 +84,9 @@ public void onSurfaceRequested(SurfaceRequest request) { flutterSurface, Executors.newSingleThreadExecutor(), (result) -> { - // See https://developer.android.com/reference/androidx/camera/core/SurfaceRequest.Result for documentation. + // See + // https://developer.android.com/reference/androidx/camera/core/SurfaceRequest.Result + // for documentation. // Always attempt a release. flutterSurface.release(); int resultCode = result.getResultCode(); @@ -104,7 +107,7 @@ public void onSurfaceRequested(SurfaceRequest request) { break; } }); - }; + } }; } @@ -112,7 +115,7 @@ public void onSurfaceRequested(SurfaceRequest request) { * Returns an error description for each {@link SurfaceRequest.Result} that represents an error * with providing a surface. */ - private String getProvideSurfaceErrorDescription(@Nullable int resultCode) { + String getProvideSurfaceErrorDescription(int resultCode) { switch (resultCode) { case SurfaceRequest.Result.RESULT_INVALID_SURFACE: return resultCode + ": Provided surface could not be used by the camera."; @@ -134,7 +137,8 @@ public void releaseFlutterSurfaceTexture() { /** Returns the resolution information for the specified {@link Preview}. */ @Override - public GeneratedCameraXLibrary.ResolutionInfo getResolutionInfo(@NonNull Long identifier) { + public @NonNull GeneratedCameraXLibrary.ResolutionInfo getResolutionInfo( + @NonNull Long identifier) { Preview preview = (Preview) Objects.requireNonNull(instanceManager.getInstance(identifier)); Size resolution = preview.getResolutionInfo().getResolution(); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderFlutterApiImpl.java index 90c94d0c26cb..35e2bc0ec73c 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderFlutterApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderFlutterApiImpl.java @@ -4,13 +4,14 @@ package io.flutter.plugins.camerax; +import androidx.annotation.NonNull; import androidx.camera.lifecycle.ProcessCameraProvider; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ProcessCameraProviderFlutterApi; public class ProcessCameraProviderFlutterApiImpl extends ProcessCameraProviderFlutterApi { public ProcessCameraProviderFlutterApiImpl( - BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { super(binaryMessenger); this.instanceManager = instanceManager; } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java index 7d72dee31eab..9292ee93fc43 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java @@ -28,13 +28,15 @@ public class ProcessCameraProviderHostApiImpl implements ProcessCameraProviderHo private LifecycleOwner lifecycleOwner; public ProcessCameraProviderHostApiImpl( - BinaryMessenger binaryMessenger, InstanceManager instanceManager, Context context) { + @NonNull BinaryMessenger binaryMessenger, + @NonNull InstanceManager instanceManager, + @NonNull Context context) { this.binaryMessenger = binaryMessenger; this.instanceManager = instanceManager; this.context = context; } - public void setLifecycleOwner(LifecycleOwner lifecycleOwner) { + public void setLifecycleOwner(@NonNull LifecycleOwner lifecycleOwner) { this.lifecycleOwner = lifecycleOwner; } @@ -45,7 +47,7 @@ public void setLifecycleOwner(LifecycleOwner lifecycleOwner) { *

If using the camera plugin in an add-to-app context, ensure that a new instance of the * {@code ProcessCameraProvider} is fetched via {@code #getInstance} anytime the context changes. */ - public void setContext(Context context) { + public void setContext(@NonNull Context context) { this.context = context; } @@ -54,7 +56,7 @@ public void setContext(Context context) { * for the current {@code Context}. */ @Override - public void getInstance(GeneratedCameraXLibrary.Result result) { + public void getInstance(@NonNull GeneratedCameraXLibrary.Result result) { ListenableFuture processCameraProviderFuture = ProcessCameraProvider.getInstance(context); @@ -78,13 +80,14 @@ public void getInstance(GeneratedCameraXLibrary.Result result) { } /** Returns cameras available to the {@code ProcessCameraProvider}. */ + @NonNull @Override public List getAvailableCameraInfos(@NonNull Long identifier) { ProcessCameraProvider processCameraProvider = (ProcessCameraProvider) Objects.requireNonNull(instanceManager.getInstance(identifier)); List availableCameras = processCameraProvider.getAvailableCameraInfos(); - List availableCamerasIds = new ArrayList(); + List availableCamerasIds = new ArrayList<>(); final CameraInfoFlutterApiImpl cameraInfoFlutterApi = new CameraInfoFlutterApiImpl(binaryMessenger, instanceManager); @@ -103,7 +106,7 @@ public List getAvailableCameraInfos(@NonNull Long identifier) { * that {@code LifecycleOwner} reflects. */ @Override - public Long bindToLifecycle( + public @NonNull Long bindToLifecycle( @NonNull Long identifier, @NonNull Long cameraSelectorIdentifier, @NonNull List useCaseIds) { @@ -128,11 +131,11 @@ public Long bindToLifecycle( cameraFlutterApi.create(camera, result -> {}); } - return instanceManager.getIdentifierForStrongReference(camera); + return Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(camera)); } @Override - public Boolean isBound(@NonNull Long identifier, @NonNull Long useCaseIdentifier) { + public @NonNull Boolean isBound(@NonNull Long identifier, @NonNull Long useCaseIdentifier) { ProcessCameraProvider processCameraProvider = (ProcessCameraProvider) Objects.requireNonNull(instanceManager.getInstance(identifier)); UseCase useCase = diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderFlutterApiImpl.java new file mode 100644 index 000000000000..5258996ca65f --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderFlutterApiImpl.java @@ -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. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.camera.video.Recorder; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.RecorderFlutterApi; + +public class RecorderFlutterApiImpl extends RecorderFlutterApi { + private final InstanceManager instanceManager; + + public RecorderFlutterApiImpl( + @Nullable BinaryMessenger binaryMessenger, @Nullable InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + void create( + @NonNull Recorder recorder, + @Nullable Long aspectRatio, + @Nullable Long bitRate, + @Nullable Reply reply) { + create(instanceManager.addHostCreatedInstance(recorder), aspectRatio, bitRate, reply); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderHostApiImpl.java new file mode 100644 index 000000000000..660c469c88b5 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderHostApiImpl.java @@ -0,0 +1,113 @@ +// 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.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.camera.video.FileOutputOptions; +import androidx.camera.video.PendingRecording; +import androidx.camera.video.Recorder; +import androidx.core.content.ContextCompat; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.RecorderHostApi; +import java.io.File; +import java.util.Objects; +import java.util.concurrent.Executor; + +public class RecorderHostApiImpl implements RecorderHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private Context context; + + @NonNull @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy(); + + @NonNull @VisibleForTesting public PendingRecordingFlutterApiImpl pendingRecordingFlutterApi; + + public RecorderHostApiImpl( + @Nullable BinaryMessenger binaryMessenger, + @NonNull InstanceManager instanceManager, + @Nullable Context context) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + this.context = context; + this.pendingRecordingFlutterApi = + new PendingRecordingFlutterApiImpl(binaryMessenger, instanceManager); + } + + @Override + public void create(@NonNull Long instanceId, @Nullable Long aspectRatio, @Nullable Long bitRate) { + Recorder.Builder recorderBuilder = cameraXProxy.createRecorderBuilder(); + if (aspectRatio != null) { + recorderBuilder.setAspectRatio(aspectRatio.intValue()); + } + if (bitRate != null) { + recorderBuilder.setTargetVideoEncodingBitRate(bitRate.intValue()); + } + Recorder recorder = recorderBuilder.setExecutor(ContextCompat.getMainExecutor(context)).build(); + instanceManager.addDartCreatedInstance(recorder, instanceId); + } + + /** Sets the context, which is used to get the {@link Executor} passed to the Recorder builder. */ + public void setContext(@Nullable Context context) { + this.context = context; + } + + /** Gets the aspect ratio of the given {@link Recorder}. */ + @NonNull + @Override + public Long getAspectRatio(@NonNull Long identifier) { + Recorder recorder = getRecorderFromInstanceId(identifier); + return Long.valueOf(recorder.getAspectRatio()); + } + + /** Gets the target video encoding bitrate of the given {@link Recorder}. */ + @NonNull + @Override + public Long getTargetVideoEncodingBitRate(@NonNull Long identifier) { + Recorder recorder = getRecorderFromInstanceId(identifier); + return Long.valueOf(recorder.getTargetVideoEncodingBitRate()); + } + + /** + * Uses the provided {@link Recorder} to prepare a recording that will be saved to a file at the + * provided path. + */ + @NonNull + @Override + public Long prepareRecording(@NonNull Long identifier, @NonNull String path) { + Recorder recorder = getRecorderFromInstanceId(identifier); + File temporaryCaptureFile = openTempFile(path); + FileOutputOptions fileOutputOptions = + new FileOutputOptions.Builder(temporaryCaptureFile).build(); + PendingRecording pendingRecording = recorder.prepareRecording(context, fileOutputOptions); + if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) + == PackageManager.PERMISSION_GRANTED) { + pendingRecording.withAudioEnabled(); + } + pendingRecordingFlutterApi.create(pendingRecording, reply -> {}); + return Objects.requireNonNull( + instanceManager.getIdentifierForStrongReference(pendingRecording)); + } + + @Nullable + @VisibleForTesting + public File openTempFile(@NonNull String path) { + File file = null; + try { + file = new File(path); + } catch (NullPointerException | SecurityException e) { + throw new RuntimeException(e); + } + return file; + } + + private Recorder getRecorderFromInstanceId(Long instanceId) { + return (Recorder) Objects.requireNonNull(instanceManager.getInstance(instanceId)); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingFlutterApiImpl.java new file mode 100644 index 000000000000..8fe7082f95c8 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingFlutterApiImpl.java @@ -0,0 +1,24 @@ +// 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.camera.video.Recording; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.RecordingFlutterApi; + +public class RecordingFlutterApiImpl extends RecordingFlutterApi { + private final InstanceManager instanceManager; + + public RecordingFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + void create(@NonNull Recording recording, Reply reply) { + create(instanceManager.addHostCreatedInstance(recording), reply); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingHostApiImpl.java new file mode 100644 index 000000000000..9288464da86b --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingHostApiImpl.java @@ -0,0 +1,50 @@ +// 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.camera.video.Recording; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.RecordingHostApi; +import java.util.Objects; + +public class RecordingHostApiImpl implements RecordingHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + + public RecordingHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + } + + @Override + public void close(@NonNull Long identifier) { + Recording recording = getRecordingFromInstanceId(identifier); + recording.close(); + } + + @Override + public void pause(@NonNull Long identifier) { + Recording recording = getRecordingFromInstanceId(identifier); + recording.pause(); + } + + @Override + public void resume(@NonNull Long identifier) { + Recording recording = getRecordingFromInstanceId(identifier); + recording.resume(); + } + + @Override + public void stop(@NonNull Long identifier) { + Recording recording = getRecordingFromInstanceId(identifier); + recording.stop(); + } + + private Recording getRecordingFromInstanceId(Long instanceId) { + return (Recording) Objects.requireNonNull(instanceManager.getInstance(instanceId)); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java index a6985811531f..b8f4d6b0c62d 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java @@ -5,37 +5,52 @@ package io.flutter.plugins.camerax; import android.app.Activity; +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraPermissionsErrorData; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.Result; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesHostApi; +import java.io.File; +import java.io.IOException; public class SystemServicesHostApiImpl implements SystemServicesHostApi { private final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; + private Context context; - @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy(); - @VisibleForTesting public DeviceOrientationManager deviceOrientationManager; - @VisibleForTesting public SystemServicesFlutterApiImpl systemServicesFlutterApi; + @VisibleForTesting public @NonNull CameraXProxy cameraXProxy = new CameraXProxy(); + @VisibleForTesting public @Nullable DeviceOrientationManager deviceOrientationManager; + @VisibleForTesting public @NonNull SystemServicesFlutterApiImpl systemServicesFlutterApi; private Activity activity; private PermissionsRegistry permissionsRegistry; public SystemServicesHostApiImpl( - BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + @NonNull BinaryMessenger binaryMessenger, + @NonNull InstanceManager instanceManager, + @NonNull Context context) { this.binaryMessenger = binaryMessenger; this.instanceManager = instanceManager; + this.context = context; this.systemServicesFlutterApi = new SystemServicesFlutterApiImpl(binaryMessenger); } - public void setActivity(Activity activity) { + /** Sets the context, which is used to get the cache directory. */ + public void setContext(@NonNull Context context) { + this.context = context; + } + + public void setActivity(@NonNull Activity activity) { this.activity = activity; } - public void setPermissionsRegistry(PermissionsRegistry permissionsRegistry) { + public void setPermissionsRegistry(@NonNull PermissionsRegistry permissionsRegistry) { this.permissionsRegistry = permissionsRegistry; } @@ -47,7 +62,7 @@ public void setPermissionsRegistry(PermissionsRegistry permissionsRegistry) { */ @Override public void requestCameraPermissions( - Boolean enableAudio, Result result) { + @NonNull Boolean enableAudio, @NonNull Result result) { CameraPermissionsManager cameraPermissionsManager = cameraXProxy.createCameraPermissionsManager(); cameraPermissionsManager.requestPermissions( @@ -70,7 +85,7 @@ public void requestCameraPermissions( } /** - * Starts listening for device orientation changes using an instace of a {@link + * Starts listening for device orientation changes using an instance of a {@link * DeviceOrientationManager}. * *

Whenever a change in device orientation is detected by the {@code DeviceOrientationManager}, @@ -78,7 +93,7 @@ public void requestCameraPermissions( */ @Override public void startListeningForDeviceOrientationChange( - Boolean isFrontFacing, Long sensorOrientation) { + @NonNull Boolean isFrontFacing, @NonNull Long sensorOrientation) { deviceOrientationManager = cameraXProxy.createDeviceOrientationManager( activity, @@ -108,4 +123,19 @@ public void stopListeningForDeviceOrientationChange() { deviceOrientationManager.stop(); } } + + /** Returns a path to be used to create a temp file in the current cache directory. */ + @Override + @NonNull + public String getTempFilePath(@NonNull String prefix, @NonNull String suffix) { + try { + File path = File.createTempFile(prefix, suffix, context.getCacheDir()); + return path.toString(); + } catch (IOException | SecurityException e) { + throw new GeneratedCameraXLibrary.FlutterError( + "getTempFilePath_failure", + "SystemServicesHostApiImpl.getTempFilePath encountered an exception: " + e.toString(), + null); + } + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureFlutterApiImpl.java new file mode 100644 index 000000000000..3e316fa57746 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureFlutterApiImpl.java @@ -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 io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.camera.video.Recorder; +import androidx.camera.video.VideoCapture; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.VideoCaptureFlutterApi; + +public class VideoCaptureFlutterApiImpl extends VideoCaptureFlutterApi { + public VideoCaptureFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + private final InstanceManager instanceManager; + + void create(@NonNull VideoCapture videoCapture, Reply reply) { + create(instanceManager.addHostCreatedInstance(videoCapture), reply); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureHostApiImpl.java new file mode 100644 index 000000000000..7e764cdff4a9 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureHostApiImpl.java @@ -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. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.camera.video.Recorder; +import androidx.camera.video.VideoCapture; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.VideoCaptureHostApi; +import java.util.Objects; + +public class VideoCaptureHostApiImpl implements VideoCaptureHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + + public VideoCaptureHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + } + + @Override + @NonNull + public Long withOutput(@NonNull Long videoOutputId) { + Recorder recorder = + (Recorder) Objects.requireNonNull(instanceManager.getInstance(videoOutputId)); + VideoCapture videoCapture = VideoCapture.withOutput(recorder); + final VideoCaptureFlutterApiImpl videoCaptureFlutterApi = + getVideoCaptureFlutterApiImpl(binaryMessenger, instanceManager); + videoCaptureFlutterApi.create(videoCapture, result -> {}); + return Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(videoCapture)); + } + + @Override + @NonNull + public Long getOutput(@NonNull Long identifier) { + VideoCapture videoCapture = + Objects.requireNonNull(instanceManager.getInstance(identifier)); + Recorder recorder = videoCapture.getOutput(); + return Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(recorder)); + } + + @VisibleForTesting + @NonNull + public VideoCaptureFlutterApiImpl getVideoCaptureFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + return new VideoCaptureFlutterApiImpl(binaryMessenger, instanceManager); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ZoomStateFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ZoomStateFlutterApiImpl.java new file mode 100644 index 000000000000..4f5abdd6d181 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ZoomStateFlutterApiImpl.java @@ -0,0 +1,38 @@ +// 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.camera.core.ZoomState; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ZoomStateFlutterApi; + +public class ZoomStateFlutterApiImpl extends ZoomStateFlutterApi { + private final InstanceManager instanceManager; + + public ZoomStateFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + /** + * Creates a {@link ZoomState} on the Dart side with its minimum zoom ratio and maximum zoom + * ratio. + */ + void create(@NonNull ZoomState zoomState, @NonNull Reply reply) { + if (instanceManager.containsInstance(zoomState)) { + return; + } + + final Float minZoomRatio = zoomState.getMinZoomRatio(); + final Float maxZoomRatio = zoomState.getMaxZoomRatio(); + create( + instanceManager.addHostCreatedInstance(zoomState), + minZoomRatio.doubleValue(), + maxZoomRatio.doubleValue(), + reply); + } +} 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 index a73654d0e69a..4a2197624b65 100644 --- 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 @@ -36,17 +36,20 @@ public void onAttachedToActivity_setsLifecycleOwnerAsActivityIfLifecycleOwner() mock(Activity.class, withSettings().extraInterfaces(LifecycleOwner.class)); ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = mock(ProcessCameraProviderHostApiImpl.class); + LiveDataHostApiImpl mockLiveDataHostApiImpl = mock(LiveDataHostApiImpl.class); doNothing().when(plugin).setUp(any(), any(), any()); when(activityPluginBinding.getActivity()).thenReturn(mockActivity); plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; + plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl; plugin.systemServicesHostApiImpl = mock(SystemServicesHostApiImpl.class); plugin.onAttachedToEngine(flutterPluginBinding); plugin.onAttachedToActivity(activityPluginBinding); verify(mockProcessCameraProviderHostApiImpl).setLifecycleOwner(any(LifecycleOwner.class)); + verify(mockLiveDataHostApiImpl).setLifecycleOwner(any(LifecycleOwner.class)); } @Test @@ -56,12 +59,14 @@ public void onAttachedToActivity_setsLifecycleOwnerAsActivityIfLifecycleOwner() Activity mockActivity = mock(Activity.class); ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = mock(ProcessCameraProviderHostApiImpl.class); + LiveDataHostApiImpl mockLiveDataHostApiImpl = mock(LiveDataHostApiImpl.class); doNothing().when(plugin).setUp(any(), any(), any()); when(activityPluginBinding.getActivity()).thenReturn(mockActivity); when(mockActivity.getApplication()).thenReturn(mock(Application.class)); plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; + plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl; plugin.systemServicesHostApiImpl = mock(SystemServicesHostApiImpl.class); plugin.onAttachedToEngine(flutterPluginBinding); @@ -69,5 +74,6 @@ public void onAttachedToActivity_setsLifecycleOwnerAsActivityIfLifecycleOwner() verify(mockProcessCameraProviderHostApiImpl) .setLifecycleOwner(any(ProxyLifecycleProvider.class)); + verify(mockLiveDataHostApiImpl).setLifecycleOwner(any(ProxyLifecycleProvider.class)); } } diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraInfoTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraInfoTest.java index 0cd85848e71a..e3cabcf4fde8 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraInfoTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraInfoTest.java @@ -7,12 +7,18 @@ 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.CameraInfo; +import androidx.camera.core.CameraState; +import androidx.camera.core.ExposureState; +import androidx.camera.core.ZoomState; +import androidx.lifecycle.LiveData; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.LiveDataSupportedType; import java.util.Objects; import org.junit.After; import org.junit.Before; @@ -41,8 +47,9 @@ public void tearDown() { } @Test - public void getSensorRotationDegreesTest() { - final CameraInfoHostApiImpl cameraInfoHostApi = new CameraInfoHostApiImpl(testInstanceManager); + public void getSensorRotationDegrees_makesCallToRetrieveSensorRotationDegrees() { + final CameraInfoHostApiImpl cameraInfoHostApi = + new CameraInfoHostApiImpl(mockBinaryMessenger, testInstanceManager); testInstanceManager.addDartCreatedInstance(mockCameraInfo, 1); @@ -53,7 +60,70 @@ public void getSensorRotationDegreesTest() { } @Test - public void flutterApiCreateTest() { + public void getCameraState_makesCallToRetrieveLiveCameraState() { + final CameraInfoHostApiImpl cameraInfoHostApiImpl = + new CameraInfoHostApiImpl(mockBinaryMessenger, testInstanceManager); + final LiveDataFlutterApiWrapper mockLiveDataFlutterApiWrapper = + mock(LiveDataFlutterApiWrapper.class); + final Long mockCameraInfoIdentifier = 27L; + @SuppressWarnings("unchecked") + final LiveData mockLiveCameraState = (LiveData) mock(LiveData.class); + + testInstanceManager.addDartCreatedInstance(mockCameraInfo, mockCameraInfoIdentifier); + cameraInfoHostApiImpl.liveDataFlutterApiWrapper = mockLiveDataFlutterApiWrapper; + when(mockCameraInfo.getCameraState()).thenReturn(mockLiveCameraState); + + final Long liveCameraStateIdentifier = + cameraInfoHostApiImpl.getCameraState(mockCameraInfoIdentifier); + + verify(mockLiveDataFlutterApiWrapper) + .create(eq(mockLiveCameraState), eq(LiveDataSupportedType.CAMERA_STATE), any()); + assertEquals( + liveCameraStateIdentifier, + testInstanceManager.getIdentifierForStrongReference(mockLiveCameraState)); + } + + @Test + public void getExposureState_retrievesExpectedExposureState() { + final CameraInfoHostApiImpl cameraInfoHostApiImpl = + new CameraInfoHostApiImpl(mockBinaryMessenger, testInstanceManager); + final ExposureState mockExposureState = mock(ExposureState.class); + final Long mockCameraInfoIdentifier = 27L; + final Long mockExposureStateIdentifier = 47L; + + testInstanceManager.addDartCreatedInstance(mockCameraInfo, mockCameraInfoIdentifier); + testInstanceManager.addDartCreatedInstance(mockExposureState, mockExposureStateIdentifier); + + when(mockCameraInfo.getExposureState()).thenReturn(mockExposureState); + + assertEquals( + cameraInfoHostApiImpl.getExposureState(mockCameraInfoIdentifier), + mockExposureStateIdentifier); + verify(mockCameraInfo).getExposureState(); + } + + @Test + @SuppressWarnings("unchecked") + public void getZoomState_retrievesExpectedZoomState() { + final CameraInfoHostApiImpl cameraInfoHostApiImpl = + new CameraInfoHostApiImpl(mockBinaryMessenger, testInstanceManager); + final LiveData mockLiveZoomState = (LiveData) mock(LiveData.class); + final ZoomState mockZoomState = mock(ZoomState.class); + final Long mockCameraInfoIdentifier = 20L; + final Long mockLiveZoomStateIdentifier = 74L; + + testInstanceManager.addDartCreatedInstance(mockCameraInfo, mockCameraInfoIdentifier); + testInstanceManager.addDartCreatedInstance(mockLiveZoomState, mockLiveZoomStateIdentifier); + + when(mockCameraInfo.getZoomState()).thenReturn(mockLiveZoomState); + + assertEquals( + cameraInfoHostApiImpl.getZoomState(mockCameraInfoIdentifier), mockLiveZoomStateIdentifier); + verify(mockCameraInfo).getZoomState(); + } + + @Test + public void flutterApiCreate_makesCallToCreateInstanceOnDartSide() { final CameraInfoFlutterApiImpl spyFlutterApi = spy(new CameraInfoFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraStateErrorTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraStateErrorTest.java new file mode 100644 index 000000000000..0d07c496c8e6 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraStateErrorTest.java @@ -0,0 +1,57 @@ +// 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.CameraState; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraStateErrorFlutterApi; +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 CameraStateErrorTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public CameraState.StateError mockCameraStateError; + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public CameraStateErrorFlutterApi mockFlutterApi; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Test + public void flutterApiCreate_makesCallToDartToCreateInstance() { + final CameraStateErrorFlutterApiWrapper flutterApi = + new CameraStateErrorFlutterApiWrapper(mockBinaryMessenger, instanceManager); + flutterApi.setApi(mockFlutterApi); + + final Long code = 0L; + + flutterApi.create(mockCameraStateError, code, reply -> {}); + + final long instanceIdentifier = + Objects.requireNonNull( + instanceManager.getIdentifierForStrongReference(mockCameraStateError)); + verify(mockFlutterApi).create(eq(instanceIdentifier), eq(code), any()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraStateTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraStateTest.java new file mode 100644 index 000000000000..726241e612cb --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraStateTest.java @@ -0,0 +1,102 @@ +// 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.junit.Assert.fail; +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 androidx.camera.core.CameraState; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraStateFlutterApi; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraStateType; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraStateTypeData; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class CameraStateTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public CameraState mockCameraState; + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public CameraStateFlutterApi mockFlutterApi; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Test + public void flutterApiCreate_makesCallToDartToCreateInstance() { + final CameraStateFlutterApiWrapper flutterApi = + new CameraStateFlutterApiWrapper(mockBinaryMessenger, instanceManager); + flutterApi.setApi(mockFlutterApi); + + final CameraStateType type = CameraStateType.OPEN; + final CameraState.StateError mockError = mock(CameraState.StateError.class); + + flutterApi.create(mockCameraState, type, mockError, reply -> {}); + + final long instanceIdentifier = + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(mockCameraState)); + final ArgumentCaptor cameraStateTypeDataCaptor = + ArgumentCaptor.forClass(CameraStateTypeData.class); + + verify(mockFlutterApi) + .create( + eq(instanceIdentifier), + cameraStateTypeDataCaptor.capture(), + eq(Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(mockError))), + any()); + + assertEquals(cameraStateTypeDataCaptor.getValue().getValue(), type); + } + + @Test + public void getCameraStateType_returnsExpectedType() { + for (CameraState.Type type : CameraState.Type.values()) { + switch (type) { + case CLOSED: + assertEquals( + CameraStateFlutterApiWrapper.getCameraStateType(type), CameraStateType.CLOSED); + break; + case CLOSING: + assertEquals( + CameraStateFlutterApiWrapper.getCameraStateType(type), CameraStateType.CLOSING); + break; + case OPEN: + assertEquals(CameraStateFlutterApiWrapper.getCameraStateType(type), CameraStateType.OPEN); + break; + case OPENING: + assertEquals( + CameraStateFlutterApiWrapper.getCameraStateType(type), CameraStateType.OPENING); + break; + case PENDING_OPEN: + assertEquals( + CameraStateFlutterApiWrapper.getCameraStateType(type), CameraStateType.PENDING_OPEN); + break; + default: + fail("The CameraState.Type " + type.toString() + " is unhandled by this test."); + } + } + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraTest.java index af4fb2ce8e61..fb6b0b3cf353 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraTest.java @@ -4,12 +4,16 @@ 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.Camera; +import androidx.camera.core.CameraInfo; import io.flutter.plugin.common.BinaryMessenger; import java.util.Objects; import org.junit.After; @@ -39,7 +43,24 @@ public void tearDown() { } @Test - public void flutterApiCreateTest() { + public void getCameraInfo_retrievesExpectedCameraInfoInstance() { + final CameraHostApiImpl cameraHostApiImpl = + new CameraHostApiImpl(mockBinaryMessenger, testInstanceManager); + final CameraInfo mockCameraInfo = mock(CameraInfo.class); + final Long cameraIdentifier = 34L; + final Long mockCameraInfoIdentifier = 97L; + + testInstanceManager.addDartCreatedInstance(camera, cameraIdentifier); + testInstanceManager.addDartCreatedInstance(mockCameraInfo, mockCameraInfoIdentifier); + + when(camera.getCameraInfo()).thenReturn(mockCameraInfo); + + assertEquals(cameraHostApiImpl.getCameraInfo(cameraIdentifier), mockCameraInfoIdentifier); + verify(camera).getCameraInfo(); + } + + @Test + public void flutterApiCreate_makesCallToCreateInstanceOnDartSide() { final CameraFlutterApiImpl spyFlutterApi = spy(new CameraFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ExposureStateTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ExposureStateTest.java new file mode 100644 index 000000000000..7eef68d28d12 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ExposureStateTest.java @@ -0,0 +1,84 @@ +// 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.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.util.Range; +import android.util.Rational; +import androidx.camera.core.ExposureState; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ExposureCompensationRange; +import java.util.Objects; +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; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +public class ExposureStateTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public ExposureState mockExposureState; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + testInstanceManager.stopFinalizationListener(); + } + + @Config(sdk = 21) + @Test + public void create_makesExpectedCallToCreateInstanceOnDartSide() { + // SDK version configured because ExposureState requires Android 21. + ExposureStateFlutterApiImpl exposureStateFlutterApiImpl = + spy(new ExposureStateFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + final int minExposureCompensation = 0; + final int maxExposureCompensation = 1; + Range testExposueCompensationRange = + new Range(minExposureCompensation, maxExposureCompensation); + Rational textExposureCompensationStep = new Rational(1, 5); // Makes expected Double value 0.2. + + when(mockExposureState.getExposureCompensationRange()).thenReturn(testExposueCompensationRange); + when(mockExposureState.getExposureCompensationStep()).thenReturn(textExposureCompensationStep); + + final ArgumentCaptor exposureCompensationRangeCaptor = + ArgumentCaptor.forClass(ExposureCompensationRange.class); + + exposureStateFlutterApiImpl.create(mockExposureState, reply -> {}); + + final long identifier = + Objects.requireNonNull( + testInstanceManager.getIdentifierForStrongReference(mockExposureState)); + verify(exposureStateFlutterApiImpl) + .create(eq(identifier), exposureCompensationRangeCaptor.capture(), eq(0.2), any()); + + ExposureCompensationRange exposureCompensationRange = + exposureCompensationRangeCaptor.getValue(); + assertEquals( + exposureCompensationRange.getMinCompensation().intValue(), minExposureCompensation); + assertEquals( + exposureCompensationRange.getMaxCompensation().intValue(), maxExposureCompensation); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageCaptureTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageCaptureTest.java index d1881b39a43b..881b6bea5a75 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageCaptureTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageCaptureTest.java @@ -190,12 +190,8 @@ public void takePicture_usesExpectedOnImageSavedCallback() { mock(ImageCapture.OutputFileResults.class); final String mockFileAbsolutePath = "absolute/path/to/captured/image"; final ImageCaptureException mockException = mock(ImageCaptureException.class); - final int testImageCaptureError = 54; - final String testExceptionMessage = "Test exception message"; imageCaptureHostApiImpl.cameraXProxy = mockCameraXProxy; - when(mockCameraXProxy.createSystemServicesFlutterApiImpl(mockBinaryMessenger)) - .thenReturn(mockSystemServicesFlutterApiImpl); when(mockFile.getAbsolutePath()).thenReturn(mockFileAbsolutePath); ImageCapture.OnImageSavedCallback onImageSavedCallback = diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/LiveDataTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/LiveDataTest.java new file mode 100644 index 000000000000..8002ff94de95 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/LiveDataTest.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. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +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.CameraState; +import androidx.camera.core.ZoomState; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.LiveDataFlutterApi; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.LiveDataSupportedType; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.LiveDataSupportedTypeData; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class LiveDataTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public LiveData mockLiveData; + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public LiveDataFlutterApi mockFlutterApi; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Test + @SuppressWarnings({"unchecked", "rawtypes"}) + public void observe_addsExpectedObserverToLiveDataInstance() { + final LiveDataHostApiImpl hostApi = + new LiveDataHostApiImpl(mockBinaryMessenger, instanceManager); + final Observer mockObserver = mock(Observer.class); + final long observerIdentifier = 20; + final long instanceIdentifier = 29; + final LifecycleOwner mockLifecycleOwner = mock(LifecycleOwner.class); + + instanceManager.addDartCreatedInstance(mockObserver, observerIdentifier); + instanceManager.addDartCreatedInstance(mockLiveData, instanceIdentifier); + + hostApi.setLifecycleOwner(mockLifecycleOwner); + hostApi.observe(instanceIdentifier, observerIdentifier); + + verify(mockLiveData).observe(mockLifecycleOwner, mockObserver); + } + + @Test + public void removeObservers_makesCallToRemoveObserversFromLiveDataInstance() { + final LiveDataHostApiImpl hostApi = + new LiveDataHostApiImpl(mockBinaryMessenger, instanceManager); + final long instanceIdentifier = 10; + final LifecycleOwner mockLifecycleOwner = mock(LifecycleOwner.class); + + instanceManager.addDartCreatedInstance(mockLiveData, instanceIdentifier); + + hostApi.setLifecycleOwner(mockLifecycleOwner); + hostApi.removeObservers(instanceIdentifier); + + verify(mockLiveData).removeObservers(mockLifecycleOwner); + } + + @Test + @SuppressWarnings("unchecked") + public void getValue_returnsExpectedValue() { + final LiveDataHostApiImpl hostApi = + new LiveDataHostApiImpl(mockBinaryMessenger, instanceManager); + + for (LiveDataSupportedType supportedType : LiveDataSupportedType.values()) { + LiveDataSupportedTypeData typeData = + new LiveDataSupportedTypeData.Builder().setValue(supportedType).build(); + + switch (supportedType) { + case CAMERA_STATE: + CameraState mockCameraState = mock(CameraState.class); + final Long mockCameraStateIdentifier = 56L; + final long instanceIdentifier = 33; + + instanceManager.addDartCreatedInstance(mockLiveData, instanceIdentifier); + instanceManager.addDartCreatedInstance(mockCameraState, mockCameraStateIdentifier); + + when(mockLiveData.getValue()).thenReturn(mockCameraState); + when(mockCameraState.getType()).thenReturn(CameraState.Type.CLOSED); + when(mockCameraState.getError()).thenReturn(null); + + assertEquals(hostApi.getValue(instanceIdentifier, typeData), mockCameraStateIdentifier); + break; + case ZOOM_STATE: + final LiveData mockLiveZoomState = (LiveData) mock(LiveData.class); + ZoomState mockZoomState = mock(ZoomState.class); + final Long mockLiveZoomStateIdentifier = 22L; + final Long mockZoomStateIdentifier = 8L; + + when(mockLiveZoomState.getValue()).thenReturn(mockZoomState); + instanceManager.addDartCreatedInstance(mockLiveZoomState, mockLiveZoomStateIdentifier); + instanceManager.addDartCreatedInstance(mockZoomState, mockZoomStateIdentifier); + + assertEquals( + hostApi.getValue(mockLiveZoomStateIdentifier, typeData), mockZoomStateIdentifier); + break; + default: + fail( + "The LiveDataSupportedType " + + supportedType.toString() + + "is unhandled by this test."); + } + } + } + + @Test + public void flutterApiCreate_makesCallToDartToCreateInstance() { + final LiveDataFlutterApiWrapper flutterApi = + new LiveDataFlutterApiWrapper(mockBinaryMessenger, instanceManager); + final LiveDataSupportedType liveDataType = LiveDataSupportedType.CAMERA_STATE; + + flutterApi.setApi(mockFlutterApi); + + final ArgumentCaptor liveDataSupportedTypeDataCaptor = + ArgumentCaptor.forClass(LiveDataSupportedTypeData.class); + + flutterApi.create(mockLiveData, liveDataType, reply -> {}); + + final long instanceIdentifier = + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(mockLiveData)); + verify(mockFlutterApi) + .create(eq(instanceIdentifier), liveDataSupportedTypeDataCaptor.capture(), any()); + assertEquals(liveDataSupportedTypeDataCaptor.getValue().getValue(), liveDataType); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ObserverTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ObserverTest.java new file mode 100644 index 000000000000..dc45c8903ecb --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ObserverTest.java @@ -0,0 +1,132 @@ +// 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.junit.Assert.assertThrows; +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.CameraState; +import androidx.camera.core.ZoomState; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraStateType; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ObserverFlutterApi; +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 ObserverTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public ObserverHostApiImpl.ObserverImpl mockObserver; + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public ObserverFlutterApi mockFlutterApi; + @Mock public ObserverHostApiImpl.ObserverProxy mockProxy; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Test + public void create_createsObserverInstance() { + final ObserverHostApiImpl hostApi = + new ObserverHostApiImpl(mockBinaryMessenger, instanceManager, mockProxy); + final long instanceIdentifier = 0; + + when(mockProxy.create(mockBinaryMessenger, instanceManager)) + .thenReturn(mockObserver); + + hostApi.create(instanceIdentifier); + + assertEquals(instanceManager.getInstance(instanceIdentifier), mockObserver); + } + + @Test + public void onChanged_makesExpectedCallToDartCallbackForCameraState() { + final ObserverFlutterApiWrapper flutterApi = + new ObserverFlutterApiWrapper(mockBinaryMessenger, instanceManager); + final ObserverHostApiImpl.ObserverImpl instance = + new ObserverHostApiImpl.ObserverImpl(mockBinaryMessenger, instanceManager); + final CameraStateFlutterApiWrapper mockCameraStateFlutterApiWrapper = + mock(CameraStateFlutterApiWrapper.class); + final long instanceIdentifier = 60; + final CameraState.StateError testCameraStateError = + CameraState.StateError.create(CameraState.ERROR_CAMERA_IN_USE); + final CameraState testCameraState = + CameraState.create(CameraState.Type.CLOSED, testCameraStateError); + final Long mockCameraStateIdentifier = instanceManager.addHostCreatedInstance(testCameraState); + + flutterApi.setApi(mockFlutterApi); + instance.setApi(flutterApi); + flutterApi.cameraStateFlutterApiWrapper = mockCameraStateFlutterApiWrapper; + + instanceManager.addDartCreatedInstance(instance, instanceIdentifier); + + instance.onChanged(testCameraState); + + verify(mockFlutterApi) + .onChanged( + eq(instanceIdentifier), eq(Objects.requireNonNull(mockCameraStateIdentifier)), any()); + verify(mockCameraStateFlutterApiWrapper) + .create(eq(testCameraState), eq(CameraStateType.CLOSED), eq(testCameraStateError), any()); + } + + @Test + public void onChanged_makesExpectedCallToDartCallbackForZoomState() { + final ObserverFlutterApiWrapper flutterApi = + new ObserverFlutterApiWrapper(mockBinaryMessenger, instanceManager); + final ObserverHostApiImpl.ObserverImpl instance = + new ObserverHostApiImpl.ObserverImpl(mockBinaryMessenger, instanceManager); + final long instanceIdentifier = 2; + final ZoomStateFlutterApiImpl mockZoomStateFlutterApiImpl = mock(ZoomStateFlutterApiImpl.class); + final ZoomState mockZoomState = mock(ZoomState.class); + final Long mockZoomStateIdentifier = instanceManager.addHostCreatedInstance(mockZoomState); + + flutterApi.setApi(mockFlutterApi); + instance.setApi(flutterApi); + flutterApi.zoomStateFlutterApiImpl = mockZoomStateFlutterApiImpl; + + instanceManager.addDartCreatedInstance(instance, instanceIdentifier); + + instance.onChanged(mockZoomState); + + verify(mockFlutterApi).onChanged(eq(instanceIdentifier), eq(mockZoomStateIdentifier), any()); + verify(mockZoomStateFlutterApiImpl).create(eq(mockZoomState), any()); + } + + @Test + public void onChanged_throwsExceptionForUnsupportedLiveDataType() { + final ObserverFlutterApiWrapper flutterApi = + new ObserverFlutterApiWrapper(mockBinaryMessenger, instanceManager); + final ObserverHostApiImpl.ObserverImpl instance = + new ObserverHostApiImpl.ObserverImpl(mockBinaryMessenger, instanceManager); + final long instanceIdentifier = 2; + + flutterApi.setApi(mockFlutterApi); + instance.setApi(flutterApi); + + instanceManager.addDartCreatedInstance(instance, instanceIdentifier); + + assertThrows(UnsupportedOperationException.class, () -> instance.onChanged(mock(Object.class))); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java new file mode 100644 index 000000000000..92415d5381a0 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java @@ -0,0 +1,107 @@ +// 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.doNothing; +import static org.mockito.Mockito.doReturn; +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 android.content.Context; +import androidx.camera.video.PendingRecording; +import androidx.camera.video.Recording; +import androidx.camera.video.VideoRecordEvent; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Objects; +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.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class PendingRecordingTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public PendingRecording mockPendingRecording; + @Mock public Recording mockRecording; + @Mock public RecordingFlutterApiImpl mockRecordingFlutterApi; + @Mock public Context mockContext; + @Mock public SystemServicesFlutterApiImpl mockSystemServicesFlutterApi; + @Mock public VideoRecordEvent.Finalize event; + @Mock public Throwable throwable; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = spy(InstanceManager.create(identifier -> {})); + } + + @After + public void tearDown() { + testInstanceManager.stopFinalizationListener(); + } + + @Test + public void testStart() { + final Long mockPendingRecordingId = 3L; + final Long mockRecordingId = testInstanceManager.addHostCreatedInstance(mockRecording); + testInstanceManager.addDartCreatedInstance(mockPendingRecording, mockPendingRecordingId); + + doReturn(mockRecording).when(mockPendingRecording).start(any(), any()); + doNothing().when(mockRecordingFlutterApi).create(any(Recording.class), any()); + PendingRecordingHostApiImpl spy = + spy(new PendingRecordingHostApiImpl(mockBinaryMessenger, testInstanceManager, mockContext)); + doReturn(mock(Executor.class)).when(spy).getExecutor(); + spy.recordingFlutterApi = mockRecordingFlutterApi; + assertEquals(spy.start(mockPendingRecordingId), mockRecordingId); + verify(mockRecordingFlutterApi).create(eq(mockRecording), any()); + + testInstanceManager.remove(mockPendingRecordingId); + testInstanceManager.remove(mockRecordingId); + } + + @Test + public void testHandleVideoRecordEventSendsError() { + PendingRecordingHostApiImpl pendingRecordingHostApi = + new PendingRecordingHostApiImpl(mockBinaryMessenger, testInstanceManager, mockContext); + pendingRecordingHostApi.systemServicesFlutterApi = mockSystemServicesFlutterApi; + final String eventMessage = "example failure message"; + + when(event.hasError()).thenReturn(true); + when(event.getCause()).thenReturn(throwable); + when(throwable.toString()).thenReturn(eventMessage); + doNothing().when(mockSystemServicesFlutterApi).sendCameraError(any(), any()); + + pendingRecordingHostApi.handleVideoRecordEvent(event); + + verify(mockSystemServicesFlutterApi).sendCameraError(eq(eventMessage), any()); + } + + @Test + public void flutterApiCreateTest() { + final PendingRecordingFlutterApiImpl spyPendingRecordingFlutterApi = + spy(new PendingRecordingFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + + spyPendingRecordingFlutterApi.create(mockPendingRecording, reply -> {}); + + final long identifier = + Objects.requireNonNull( + testInstanceManager.getIdentifierForStrongReference(mockPendingRecording)); + verify(spyPendingRecordingFlutterApi).create(eq(identifier), any()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java index abf992573681..39b73abd7381 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java @@ -148,10 +148,12 @@ public void createSurfaceProvider_createsExpectedPreviewSurfaceProvider() { verify(mockSurfaceRequest) .provideSurface(surfaceCaptor.capture(), any(Executor.class), consumerCaptor.capture()); - // Test that the surface derived from the surface texture entry will be provided to the surface request. + // Test that the surface derived from the surface texture entry will be provided to the surface + // request. assertEquals(surfaceCaptor.getValue(), mockSurface); - // Test that the Consumer used to handle surface request result releases Flutter surface texture appropriately + // Test that the Consumer used to handle surface request result releases Flutter surface texture + // appropriately // and sends camera errors appropriately. Consumer capturedConsumer = consumerCaptor.getValue(); diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecorderTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecorderTest.java new file mode 100644 index 000000000000..72355d260c54 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecorderTest.java @@ -0,0 +1,169 @@ +// 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.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +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 android.content.Context; +import androidx.camera.video.FileOutputOptions; +import androidx.camera.video.PendingRecording; +import androidx.camera.video.Recorder; +import androidx.test.core.app.ApplicationProvider; +import io.flutter.plugin.common.BinaryMessenger; +import java.io.File; +import java.util.Objects; +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.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class RecorderTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public Recorder mockRecorder; + private Context context; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = spy(InstanceManager.create(identifier -> {})); + context = ApplicationProvider.getApplicationContext(); + } + + @After + public void tearDown() { + testInstanceManager.stopFinalizationListener(); + } + + @Test + public void createTest() { + final int recorderId = 0; + final int aspectRatio = 1; + final int bitRate = 2; + + final RecorderHostApiImpl recorderHostApi = + new RecorderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + + final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); + final Recorder.Builder mockRecorderBuilder = mock(Recorder.Builder.class); + recorderHostApi.cameraXProxy = mockCameraXProxy; + when(mockCameraXProxy.createRecorderBuilder()).thenReturn(mockRecorderBuilder); + when(mockRecorderBuilder.setAspectRatio(aspectRatio)).thenReturn(mockRecorderBuilder); + when(mockRecorderBuilder.setTargetVideoEncodingBitRate(bitRate)) + .thenReturn(mockRecorderBuilder); + when(mockRecorderBuilder.setExecutor(any(Executor.class))).thenReturn(mockRecorderBuilder); + when(mockRecorderBuilder.build()).thenReturn(mockRecorder); + + recorderHostApi.create( + Long.valueOf(recorderId), Long.valueOf(aspectRatio), Long.valueOf(bitRate)); + verify(mockCameraXProxy).createRecorderBuilder(); + verify(mockRecorderBuilder).setAspectRatio(aspectRatio); + verify(mockRecorderBuilder).setTargetVideoEncodingBitRate(bitRate); + verify(mockRecorderBuilder).build(); + assertEquals(testInstanceManager.getInstance(Long.valueOf(recorderId)), mockRecorder); + testInstanceManager.remove(Long.valueOf(recorderId)); + } + + @Test + public void getAspectRatioTest() { + final int recorderId = 3; + final int aspectRatio = 6; + + when(mockRecorder.getAspectRatio()).thenReturn(aspectRatio); + testInstanceManager.addDartCreatedInstance(mockRecorder, Long.valueOf(recorderId)); + final RecorderHostApiImpl recorderHostApi = + new RecorderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + assertEquals( + recorderHostApi.getAspectRatio(Long.valueOf(recorderId)), Long.valueOf(aspectRatio)); + verify(mockRecorder).getAspectRatio(); + testInstanceManager.remove(Long.valueOf(recorderId)); + } + + @Test + public void getTargetVideoEncodingBitRateTest() { + final int bitRate = 7; + final int recorderId = 3; + + when(mockRecorder.getTargetVideoEncodingBitRate()).thenReturn(bitRate); + testInstanceManager.addDartCreatedInstance(mockRecorder, Long.valueOf(recorderId)); + final RecorderHostApiImpl recorderHostApi = + new RecorderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + assertEquals( + recorderHostApi.getTargetVideoEncodingBitRate(Long.valueOf(recorderId)), + Long.valueOf(bitRate)); + verify(mockRecorder).getTargetVideoEncodingBitRate(); + testInstanceManager.remove(Long.valueOf(recorderId)); + } + + @Test + @SuppressWarnings("unchecked") + public void prepareRecording_returnsExpectedPendingRecording() { + final int recorderId = 3; + + PendingRecordingFlutterApiImpl mockPendingRecordingFlutterApi = + mock(PendingRecordingFlutterApiImpl.class); + PendingRecording mockPendingRecording = mock(PendingRecording.class); + testInstanceManager.addDartCreatedInstance(mockRecorder, Long.valueOf(recorderId)); + when(mockRecorder.prepareRecording(any(Context.class), any(FileOutputOptions.class))) + .thenReturn(mockPendingRecording); + doNothing().when(mockPendingRecordingFlutterApi).create(any(PendingRecording.class), any()); + Long mockPendingRecordingId = testInstanceManager.addHostCreatedInstance(mockPendingRecording); + + RecorderHostApiImpl spy = + spy(new RecorderHostApiImpl(mockBinaryMessenger, testInstanceManager, context)); + spy.pendingRecordingFlutterApi = mockPendingRecordingFlutterApi; + doReturn(mock(File.class)).when(spy).openTempFile(any()); + spy.prepareRecording(Long.valueOf(recorderId), ""); + + testInstanceManager.remove(Long.valueOf(recorderId)); + testInstanceManager.remove(mockPendingRecordingId); + } + + @Test + @SuppressWarnings("unchecked") + public void prepareRecording_errorsWhenPassedNullPath() { + final int recorderId = 3; + + testInstanceManager.addDartCreatedInstance(mockRecorder, Long.valueOf(recorderId)); + RecorderHostApiImpl recorderHostApi = + new RecorderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + assertThrows( + RuntimeException.class, + () -> { + recorderHostApi.prepareRecording(Long.valueOf(recorderId), null); + }); + testInstanceManager.remove(Long.valueOf(recorderId)); + } + + @Test + public void flutterApiCreateTest() { + final RecorderFlutterApiImpl spyRecorderFlutterApi = + spy(new RecorderFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + + spyRecorderFlutterApi.create(mockRecorder, null, null, reply -> {}); + + final long identifier = + Objects.requireNonNull(testInstanceManager.getIdentifierForStrongReference(mockRecorder)); + verify(spyRecorderFlutterApi).create(eq(identifier), eq(null), eq(null), any()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecordingTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecordingTest.java new file mode 100644 index 000000000000..ca0c99649561 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecordingTest.java @@ -0,0 +1,111 @@ +// 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.spy; +import static org.mockito.Mockito.verify; + +import androidx.camera.video.Recording; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class RecordingTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public Recording mockRecording; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = spy(InstanceManager.create(identifier -> {})); + } + + @After + public void tearDown() { + testInstanceManager.stopFinalizationListener(); + } + + @Test + public void close_getsRecordingFromInstanceManagerAndCloses() { + final RecordingHostApiImpl recordingHostApi = + new RecordingHostApiImpl(mockBinaryMessenger, testInstanceManager); + final Long recordingId = 5L; + + testInstanceManager.addDartCreatedInstance(mockRecording, recordingId); + + recordingHostApi.close(recordingId); + + verify(mockRecording).close(); + testInstanceManager.remove(recordingId); + } + + @Test + public void stop_getsRecordingFromInstanceManagerAndStops() { + final RecordingHostApiImpl recordingHostApi = + new RecordingHostApiImpl(mockBinaryMessenger, testInstanceManager); + final Long recordingId = 5L; + + testInstanceManager.addDartCreatedInstance(mockRecording, recordingId); + + recordingHostApi.stop(recordingId); + + verify(mockRecording).stop(); + testInstanceManager.remove(recordingId); + } + + @Test + public void resume_getsRecordingFromInstanceManagerAndResumes() { + final RecordingHostApiImpl recordingHostApi = + new RecordingHostApiImpl(mockBinaryMessenger, testInstanceManager); + final Long recordingId = 5L; + + testInstanceManager.addDartCreatedInstance(mockRecording, recordingId); + + recordingHostApi.resume(recordingId); + + verify(mockRecording).resume(); + testInstanceManager.remove(recordingId); + } + + @Test + public void pause_getsRecordingFromInstanceManagerAndPauses() { + final RecordingHostApiImpl recordingHostApi = + new RecordingHostApiImpl(mockBinaryMessenger, testInstanceManager); + final Long recordingId = 5L; + + testInstanceManager.addDartCreatedInstance(mockRecording, recordingId); + + recordingHostApi.pause(recordingId); + + verify(mockRecording).pause(); + testInstanceManager.remove(recordingId); + } + + @Test + public void flutterApiCreateTest() { + final RecordingFlutterApiImpl spyRecordingFlutterApi = + spy(new RecordingFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + + spyRecordingFlutterApi.create(mockRecording, reply -> {}); + + final long identifier = + Objects.requireNonNull(testInstanceManager.getIdentifierForStrongReference(mockRecording)); + verify(spyRecordingFlutterApi).create(eq(identifier), any()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java index 562fbe8b526d..f905704cbc10 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java @@ -5,14 +5,17 @@ package io.flutter.plugins.camerax; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Activity; +import android.content.Context; import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry; @@ -21,11 +24,14 @@ import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraPermissionsErrorData; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.Result; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi.Reply; +import java.io.File; +import java.io.IOException; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -34,11 +40,12 @@ public class SystemServicesTest { @Mock public BinaryMessenger mockBinaryMessenger; @Mock public InstanceManager mockInstanceManager; + @Mock public Context mockContext; @Test public void requestCameraPermissionsTest() { final SystemServicesHostApiImpl systemServicesHostApi = - new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager); + new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext); final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); final CameraPermissionsManager mockCameraPermissionsManager = mock(CameraPermissionsManager.class); @@ -92,7 +99,7 @@ public void requestCameraPermissionsTest() { @Test public void deviceOrientationChangeTest() { final SystemServicesHostApiImpl systemServicesHostApi = - new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager); + new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext); final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); final Activity mockActivity = mock(Activity.class); final DeviceOrientationManager mockDeviceOrientationManager = @@ -137,4 +144,44 @@ public void deviceOrientationChangeTest() { // Test that the DeviceOrientationManager starts listening for device orientation changes. verify(mockDeviceOrientationManager).start(); } + + @Test + public void getTempFilePath_returnsCorrectPath() { + final SystemServicesHostApiImpl systemServicesHostApi = + new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext); + + final String prefix = "prefix"; + final String suffix = ".suffix"; + final MockedStatic mockedStaticFile = mockStatic(File.class); + final File mockOutputDir = mock(File.class); + final File mockFile = mock(File.class); + when(mockContext.getCacheDir()).thenReturn(mockOutputDir); + mockedStaticFile + .when(() -> File.createTempFile(prefix, suffix, mockOutputDir)) + .thenReturn(mockFile); + when(mockFile.toString()).thenReturn(prefix + suffix); + assertEquals(systemServicesHostApi.getTempFilePath(prefix, suffix), prefix + suffix); + + mockedStaticFile.close(); + } + + @Test + public void getTempFilePath_throwsRuntimeExceptionOnIOException() { + final SystemServicesHostApiImpl systemServicesHostApi = + new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext); + + final String prefix = "prefix"; + final String suffix = ".suffix"; + final MockedStatic mockedStaticFile = mockStatic(File.class); + final File mockOutputDir = mock(File.class); + when(mockContext.getCacheDir()).thenReturn(mockOutputDir); + mockedStaticFile + .when(() -> File.createTempFile(prefix, suffix, mockOutputDir)) + .thenThrow(IOException.class); + assertThrows( + GeneratedCameraXLibrary.FlutterError.class, + () -> systemServicesHostApi.getTempFilePath(prefix, suffix)); + + mockedStaticFile.close(); + } } diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoCaptureTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoCaptureTest.java new file mode 100644 index 000000000000..95794334c134 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoCaptureTest.java @@ -0,0 +1,92 @@ +// 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.spy; +import static org.mockito.Mockito.verify; + +import androidx.camera.video.Recorder; +import androidx.camera.video.VideoCapture; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class VideoCaptureTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public Recorder mockRecorder; + @Mock public VideoCaptureFlutterApiImpl mockVideoCaptureFlutterApi; + @Mock public VideoCapture mockVideoCapture; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = spy(InstanceManager.create(identifier -> {})); + } + + @After + public void tearDown() { + testInstanceManager.stopFinalizationListener(); + } + + @Test + public void getOutput_returnsAssociatedRecorder() { + final Long recorderId = 5L; + final Long videoCaptureId = 6L; + VideoCapture videoCapture = VideoCapture.withOutput(mockRecorder); + + testInstanceManager.addDartCreatedInstance(mockRecorder, recorderId); + testInstanceManager.addDartCreatedInstance(videoCapture, videoCaptureId); + + VideoCaptureHostApiImpl videoCaptureHostApi = + new VideoCaptureHostApiImpl(mockBinaryMessenger, testInstanceManager); + assertEquals(videoCaptureHostApi.getOutput(videoCaptureId), recorderId); + testInstanceManager.remove(recorderId); + testInstanceManager.remove(videoCaptureId); + } + + @Test + @SuppressWarnings("unchecked") + public void withOutput_returnsNewVideoCaptureWithAssociatedRecorder() { + final Long recorderId = 5L; + testInstanceManager.addDartCreatedInstance(mockRecorder, recorderId); + + VideoCaptureHostApiImpl videoCaptureHostApi = + new VideoCaptureHostApiImpl(mockBinaryMessenger, testInstanceManager); + VideoCaptureHostApiImpl spyVideoCaptureApi = spy(videoCaptureHostApi); + final Long videoCaptureId = videoCaptureHostApi.withOutput(recorderId); + VideoCapture videoCapture = testInstanceManager.getInstance(videoCaptureId); + assertEquals(videoCapture.getOutput(), mockRecorder); + + testInstanceManager.remove(recorderId); + testInstanceManager.remove(videoCaptureId); + } + + @Test + public void flutterApiCreateTest() { + final VideoCaptureFlutterApiImpl spyVideoCaptureFlutterApi = + spy(new VideoCaptureFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + spyVideoCaptureFlutterApi.create(mockVideoCapture, reply -> {}); + + final long identifier = + Objects.requireNonNull( + testInstanceManager.getIdentifierForStrongReference(mockVideoCapture)); + verify(spyVideoCaptureFlutterApi).create(eq(identifier), any()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ZoomStateTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ZoomStateTest.java new file mode 100644 index 000000000000..a4f53ad80c5d --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ZoomStateTest.java @@ -0,0 +1,63 @@ +// 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.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.camera.core.ZoomState; +import io.flutter.plugin.common.BinaryMessenger; +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 ZoomStateTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public ZoomState mockZoomState; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + testInstanceManager.stopFinalizationListener(); + } + + @Test + public void create_makesExpectedCallToCreateInstanceOnDartSide() { + ZoomStateFlutterApiImpl zoomStateFlutterApiImpl = + spy(new ZoomStateFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + final Float testMinZoomRatio = 0F; + final Float testMaxZoomRatio = 1F; + + when(mockZoomState.getMinZoomRatio()).thenReturn(testMinZoomRatio); + when(mockZoomState.getMaxZoomRatio()).thenReturn(testMaxZoomRatio); + + zoomStateFlutterApiImpl.create(mockZoomState, reply -> {}); + + final long identifier = + Objects.requireNonNull(testInstanceManager.getIdentifierForStrongReference(mockZoomState)); + verify(zoomStateFlutterApiImpl) + .create( + eq(identifier), + eq(testMinZoomRatio.doubleValue()), + eq(testMaxZoomRatio.doubleValue()), + any()); + } +} diff --git a/packages/camera/camera_android_camerax/example/.pluginToolsConfig.yaml b/packages/camera/camera_android_camerax/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/camera/camera_android_camerax/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/camera/camera_android_camerax/example/android/build.gradle b/packages/camera/camera_android_camerax/example/android/build.gradle index ff7e7df1891b..702f1d280649 100644 --- a/packages/camera/camera_android_camerax/example/android/build.gradle +++ b/packages/camera/camera_android_camerax/example/android/build.gradle @@ -1,5 +1,8 @@ buildscript { - ext.kotlin_version = '1.8.0' + // This version should intentionally be a 1.7.* version and lower than the + // version of kotlin-bom defined in packages/camera/camera_android_camerax/android/build.gradle. + // This tests that the kotlin version resolution continues to work. + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index c668871b7a62..1e051d4582ec 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -253,9 +253,7 @@ class _CameraExampleHomeState extends State child: Center( child: AspectRatio( aspectRatio: - localVideoController.value.size != null - ? localVideoController.value.aspectRatio - : 1.0, + localVideoController.value.aspectRatio, child: VideoPlayer(localVideoController)), ), ), @@ -514,7 +512,8 @@ class _CameraExampleHomeState extends State IconButton( icon: const Icon(Icons.videocam), color: Colors.blue, - onPressed: () {}, // TODO(camsim99): Add functionality back here. + onPressed: + cameraController == null ? null : onVideoRecordButtonPressed, ), IconButton( icon: cameraController != null && @@ -522,12 +521,20 @@ class _CameraExampleHomeState extends State ? const Icon(Icons.play_arrow) : const Icon(Icons.pause), color: Colors.blue, - onPressed: () {}, // TODO(camsim99): Add functionality back here. + onPressed: () { + if (cameraController == null) { + return; + } else if (cameraController.value.isRecordingPaused) { + return onResumeButtonPressed(); + } else { + return onPauseButtonPressed(); + } + }, ), IconButton( icon: const Icon(Icons.stop), color: Colors.red, - onPressed: () {}, // TODO(camsim99): Add functionality back here. + onPressed: cameraController == null ? null : onStopButtonPressed, ), IconButton( icon: const Icon(Icons.pause_presentation), @@ -976,7 +983,7 @@ class _CameraExampleHomeState extends State : VideoPlayerController.file(File(videoFile!.path)); videoPlayerListener = () { - if (videoController != null && videoController!.value.size != null) { + if (videoController != null) { // Refreshing the state to update video player with the correct ratio. if (mounted) { setState(() {}); diff --git a/packages/camera/camera_android_camerax/lib/src/analyzer.dart b/packages/camera/camera_android_camerax/lib/src/analyzer.dart index fc312cd1982f..8cdcc137ae4c 100644 --- a/packages/camera/camera_android_camerax/lib/src/analyzer.dart +++ b/packages/camera/camera_android_camerax/lib/src/analyzer.dart @@ -87,35 +87,40 @@ class _AnalyzerHostApiImpl extends AnalyzerHostApi { @protected class AnalyzerFlutterApiImpl implements AnalyzerFlutterApi { /// Constructs a [AnalyzerFlutterApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. AnalyzerFlutterApiImpl({ - this.binaryMessenger, + BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + }) : _binaryMessenger = binaryMessenger, + _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; + final BinaryMessenger? _binaryMessenger; /// Maintains instances stored to communicate with native language objects. - final InstanceManager instanceManager; + final InstanceManager _instanceManager; @override void create( int identifier, ) { - instanceManager.addHostCreatedInstance( + _instanceManager.addHostCreatedInstance( Analyzer.detached( analyze: (ImageProxy imageProxy) async {}, - binaryMessenger: binaryMessenger, - instanceManager: instanceManager, + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, ), identifier, onCopy: (Analyzer original) => Analyzer.detached( analyze: original.analyze, - binaryMessenger: binaryMessenger, - instanceManager: instanceManager, + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, ), ); } @@ -126,9 +131,9 @@ class AnalyzerFlutterApiImpl implements AnalyzerFlutterApi { int imageProxyIdentifier, ) { final Analyzer instance = - instanceManager.getInstanceWithWeakReference(identifier)!; + _instanceManager.getInstanceWithWeakReference(identifier)!; final ImageProxy imageProxy = - instanceManager.getInstanceWithWeakReference(imageProxyIdentifier)!; + _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 7975c9851074..959f35b0980e 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 @@ -4,6 +4,7 @@ import 'dart:async'; +import 'package:async/async.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -12,19 +13,40 @@ import 'analyzer.dart'; import 'camera.dart'; import 'camera_info.dart'; import 'camera_selector.dart'; +import 'camera_state.dart'; import 'camerax_library.g.dart'; +import 'exposure_state.dart'; import 'image_analysis.dart'; import 'image_capture.dart'; import 'image_proxy.dart'; +import 'live_data.dart'; +import 'observer.dart'; +import 'pending_recording.dart'; import 'plane_proxy.dart'; import 'preview.dart'; import 'process_camera_provider.dart'; +import 'recorder.dart'; +import 'recording.dart'; import 'surface.dart'; import 'system_services.dart'; import 'use_case.dart'; +import 'video_capture.dart'; +import 'zoom_state.dart'; /// The Android implementation of [CameraPlatform] that uses the CameraX library. class AndroidCameraCameraX extends CameraPlatform { + /// Constructs an [AndroidCameraCameraX]. + AndroidCameraCameraX() : _shouldCreateDetachedObjectForTesting = false; + + /// Constructs an [AndroidCameraCameraX] that is able to set + /// [_shouldCreateDetachedObjectForTesting] to create detached objects + /// for testing purposes only. + @visibleForTesting + AndroidCameraCameraX.forTesting( + {bool shouldCreateDetachedObjectForTesting = false}) + : _shouldCreateDetachedObjectForTesting = + shouldCreateDetachedObjectForTesting; + /// Registers this class as the default instance of [CameraPlatform]. static void registerWith() { CameraPlatform.instance = AndroidCameraCameraX(); @@ -39,12 +61,45 @@ class AndroidCameraCameraX extends CameraPlatform { @visibleForTesting Camera? camera; + /// The [CameraInfo] instance that corresponds to the [camera] instance. + @visibleForTesting + CameraInfo? cameraInfo; + + /// The [LiveData] of the [CameraState] that represents the state of the + /// [camera] instance. + LiveData? liveCameraState; + /// The [Preview] instance that can be configured to present a live camera preview. @visibleForTesting Preview? preview; + /// The [VideoCapture] instance that can be instantiated and configured to + /// handle video recording + @visibleForTesting + VideoCapture? videoCapture; + + /// The [Recorder] instance handling the current creating a new [PendingRecording]. + @visibleForTesting + Recorder? recorder; + + /// The [PendingRecording] instance used to create an active [Recording]. + @visibleForTesting + PendingRecording? pendingRecording; + + /// The [Recording] instance representing the current recording. + @visibleForTesting + Recording? recording; + + /// The path at which the video file will be saved for the current [Recording]. + @visibleForTesting + String? videoOutputPath; + bool _previewIsPaused = false; + /// The prefix used to create the filename for video recording files. + @visibleForTesting + final String videoPrefix = 'MOV'; + /// The [ImageCapture] instance that can be configured to capture a still image. @visibleForTesting ImageCapture? imageCapture; @@ -77,15 +132,14 @@ class AndroidCameraCameraX extends CameraPlatform { cameraEventStreamController.stream .where((CameraEvent event) => event.cameraId == cameraId); + /// Conditional used to create detached objects for testing their + /// callback methods. + final bool _shouldCreateDetachedObjectForTesting; + /// 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. @@ -96,6 +150,10 @@ class AndroidCameraCameraX extends CameraPlatform { /// See https://developer.android.com/reference/android/graphics/ImageFormat#JPEG. static const int imageFormatJpeg = 256; + /// Error code indicating a [ZoomState] was requested, but one has not been + /// set for the camera in use. + static const String zoomStateNotSetErrorCode = 'zoomStateNotSet'; + /// Returns list of all available cameras and their descriptions. @override Future> availableCameras() async { @@ -143,7 +201,7 @@ class AndroidCameraCameraX extends CameraPlatform { /// /// In the CameraX library, cameras are accessed by combining [UseCase]s /// to an instance of a [ProcessCameraProvider]. Thus, to create an - /// unitialized camera instance, this method retrieves a + /// uninitialized camera instance, this method retrieves a /// [ProcessCameraProvider] instance. /// /// To return the camera ID, which is equivalent to the ID of the surface texture @@ -186,10 +244,18 @@ class AndroidCameraCameraX extends CameraPlatform { _getTargetResolutionForImageCapture(_resolutionPreset); imageCapture = createImageCapture(null, imageCaptureTargetResolution); + // Configure VideoCapture and Recorder instances. + // TODO(gmackall): Enable video capture resolution configuration in createRecorder(). + recorder = createRecorder(); + videoCapture = await createVideoCapture(recorder!); + // Bind configured UseCases to ProcessCameraProvider instance & mark Preview - // instance as bound but not paused. + // instance as bound but not paused. Video capture is bound at first use + // instead of here. camera = await processCameraProvider! .bindToLifecycle(cameraSelector!, [preview!, imageCapture!]); + await _updateLiveCameraState(flutterSurfaceTextureId); + cameraInfo = await camera!.getCameraInfo(); _previewIsPaused = false; return flutterSurfaceTextureId; @@ -251,7 +317,9 @@ class AndroidCameraCameraX extends CameraPlatform { @override Future dispose(int cameraId) async { preview?.releaseFlutterSurfaceTexture(); + liveCameraState?.removeObservers(); processCameraProvider?.unbindAll(); + imageAnalysis?.clearAnalyzer(); } /// The camera has been initialized. @@ -260,13 +328,88 @@ class AndroidCameraCameraX extends CameraPlatform { return _cameraEvents(cameraId).whereType(); } + /// The camera started to close. + @override + Stream onCameraClosing(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + /// The camera experienced an error. @override Stream onCameraError(int cameraId) { - return SystemServices.cameraErrorStreamController.stream - .map((String errorDescription) { - return CameraErrorEvent(cameraId, errorDescription); - }); + return StreamGroup.mergeBroadcast< + CameraErrorEvent>(>[ + SystemServices.cameraErrorStreamController.stream + .map((String errorDescription) { + return CameraErrorEvent(cameraId, errorDescription); + }), + _cameraEvents(cameraId).whereType() + ]); + } + + /// Gets the minimum supported exposure offset for the selected camera in EV units. + /// + /// [cameraId] not used. + @override + Future getMinExposureOffset(int cameraId) async { + final ExposureState exposureState = await cameraInfo!.getExposureState(); + return exposureState.exposureCompensationRange.minCompensation * + exposureState.exposureCompensationStep; + } + + /// Gets the maximum supported exposure offset for the selected camera in EV units. + /// + /// [cameraId] not used. + @override + Future getMaxExposureOffset(int cameraId) async { + final ExposureState exposureState = await cameraInfo!.getExposureState(); + return exposureState.exposureCompensationRange.maxCompensation * + exposureState.exposureCompensationStep; + } + + /// Gets the supported step size for exposure offset for the selected camera in EV units. + /// + /// Returns 0 when exposure compensation is not supported. + /// + /// [cameraId] not used. + @override + Future getExposureOffsetStepSize(int cameraId) async { + final ExposureState exposureState = await cameraInfo!.getExposureState(); + return exposureState.exposureCompensationStep; + } + + /// Gets the maximum supported zoom level for the selected camera. + /// + /// [cameraId] not used. + @override + Future getMaxZoomLevel(int cameraId) async { + final LiveData liveZoomState = await cameraInfo!.getZoomState(); + final ZoomState? zoomState = await liveZoomState.getValue(); + + if (zoomState == null) { + throw CameraException( + zoomStateNotSetErrorCode, + 'No explicit ZoomState has been set on the LiveData instance for the camera in use.', + ); + } + return zoomState.maxZoomRatio; + } + + /// Gets the minimum supported zoom level for the selected camera. + /// + /// [cameraId] not used. + @override + Future getMinZoomLevel(int cameraId) async { + final LiveData liveZoomState = await cameraInfo!.getZoomState(); + final ZoomState? zoomState = await liveZoomState.getValue(); + + if (zoomState == null) { + throw CameraException( + zoomStateNotSetErrorCode, + 'No explicit ZoomState has been set on the LiveData instance for the camera in use.', + ); + } + return zoomState.minZoomRatio; } /// The ui orientation changed. @@ -289,7 +432,7 @@ class AndroidCameraCameraX extends CameraPlatform { /// [cameraId] not used. @override Future resumePreview(int cameraId) async { - await _bindPreviewToLifecycle(); + await _bindPreviewToLifecycle(cameraId); _previewIsPaused = false; } @@ -297,7 +440,7 @@ class AndroidCameraCameraX extends CameraPlatform { @override Widget buildPreview(int cameraId) { return FutureBuilder( - future: _bindPreviewToLifecycle(), + future: _bindPreviewToLifecycle(cameraId), builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: @@ -323,6 +466,78 @@ class AndroidCameraCameraX extends CameraPlatform { return XFile(picturePath); } + /// Configures and starts a video recording. Returns silently without doing + /// anything if there is currently an active recording. + @override + Future startVideoRecording(int cameraId, + {Duration? maxVideoDuration}) async { + assert(cameraSelector != null); + assert(processCameraProvider != null); + + if (recording != null) { + // There is currently an active recording, so do not start a new one. + return; + } + + if (!(await processCameraProvider!.isBound(videoCapture!))) { + camera = await processCameraProvider! + .bindToLifecycle(cameraSelector!, [videoCapture!]); + } + + videoOutputPath = + await SystemServices.getTempFilePath(videoPrefix, '.temp'); + pendingRecording = await recorder!.prepareRecording(videoOutputPath!); + recording = await pendingRecording!.start(); + } + + /// Stops the video recording and returns the file where it was saved. + /// Throws a CameraException if the recording is currently null, or if the + /// videoOutputPath is null. + /// + /// If the videoOutputPath is null the recording objects are cleaned up + /// so starting a new recording is possible. + @override + Future stopVideoRecording(int cameraId) async { + if (recording == null) { + throw CameraException( + 'videoRecordingFailed', + 'Attempting to stop a ' + 'video recording while no recording is in progress.'); + } + if (videoOutputPath == null) { + // Stop the current active recording as we will be unable to complete it + // in this error case. + recording!.close(); + recording = null; + pendingRecording = null; + throw CameraException( + 'INVALID_PATH', + 'The platform did not return a path ' + 'while reporting success. The platform should always ' + 'return a valid path or report an error.'); + } + recording!.close(); + recording = null; + pendingRecording = null; + return XFile(videoOutputPath!); + } + + /// Pause the current video recording if it is not null. + @override + Future pauseVideoRecording(int cameraId) async { + if (recording != null) { + recording!.pause(); + } + } + + /// Resume the current video recording if it is not null. + @override + Future resumeVideoRecording(int cameraId) async { + if (recording != null) { + recording!.resume(); + } + } + /// A new streamed frame is available. /// /// Listening to this stream will start streaming, and canceling will stop. @@ -348,7 +563,11 @@ class AndroidCameraCameraX extends CameraPlatform { /// Binds [preview] instance to the camera lifecycle controlled by the /// [processCameraProvider]. - Future _bindPreviewToLifecycle() async { + /// + /// [cameraId] used to build [CameraEvent]s should you wish to filter + /// these based on a reference to a cameraId received from calling + /// `createCamera(...)`. + Future _bindPreviewToLifecycle(int cameraId) async { final bool previewIsBound = await processCameraProvider!.isBound(preview!); if (previewIsBound || _previewIsPaused) { // Only bind if preview is not already bound or intentionally paused. @@ -357,6 +576,8 @@ class AndroidCameraCameraX extends CameraPlatform { camera = await processCameraProvider! .bindToLifecycle(cameraSelector!, [preview!]); + await _updateLiveCameraState(cameraId); + cameraInfo = await camera!.getCameraInfo(); } /// Configures the [imageAnalysis] instance for image streaming and binds it @@ -369,6 +590,8 @@ class AndroidCameraCameraX extends CameraPlatform { } // Create Analyzer that can read image data for image streaming. + final WeakReference weakThis = + WeakReference(this); Future analyze(ImageProxy imageProxy) async { final List planes = await imageProxy.getPlanes(); final List cameraImagePlanes = []; @@ -389,11 +612,15 @@ class AndroidCameraCameraX extends CameraPlatform { planes: cameraImagePlanes, height: imageProxy.height, width: imageProxy.width); - cameraImageDataStreamController?.add(cameraImageData); + + weakThis.target!.cameraImageDataStreamController!.add(cameraImageData); imageProxy.close(); } - final Analyzer analyzer = createDetachedCallbacks + // shouldCreateDetachedObjectForTesting is used to create an Analyzer + // detached from the native sideonly to test the logic of the Analyzer + // instance that will be used for image streaming. + final Analyzer analyzer = _shouldCreateDetachedObjectForTesting ? Analyzer.detached(analyze: analyze) : Analyzer(analyze: analyze); @@ -406,6 +633,7 @@ class AndroidCameraCameraX extends CameraPlatform { // https://github.com/flutter/packages/pull/3419 lands. camera = await processCameraProvider! .bindToLifecycle(cameraSelector!, [imageAnalysis!]); + cameraInfo = await camera!.getCameraInfo(); } /// Unbinds [useCase] from camera lifecycle controlled by the @@ -423,7 +651,7 @@ class AndroidCameraCameraX extends CameraPlatform { /// The [onListen] callback for the stream controller used for image /// streaming. - void _onFrameStreamListen() { + Future _onFrameStreamListen() async { _configureAndBindImageAnalysisToLifecycle(); } @@ -450,6 +678,54 @@ class AndroidCameraCameraX extends CameraPlatform { return ImageFormatGroup.unknown; } + // Methods concerning camera state: + + /// Adds observers to the [LiveData] of the [CameraState] of the current + /// [camera], saved as [liveCameraState]. + /// + /// If a previous [liveCameraState] was stored, existing observers are + /// removed, as well. + Future _updateLiveCameraState(int cameraId) async { + final CameraInfo cameraInfo = await camera!.getCameraInfo(); + liveCameraState?.removeObservers(); + liveCameraState = await cameraInfo.getCameraState(); + await liveCameraState!.observe(_createCameraClosingObserver(cameraId)); + } + + /// Creates [Observer] of the [CameraState] that will: + /// + /// * Send a [CameraClosingEvent] if the [CameraState] indicates that the + /// camera has begun to close. + /// * Send a [CameraErrorEvent] if the [CameraState] indicates that the + /// camera is in error state. + Observer _createCameraClosingObserver(int cameraId) { + final WeakReference weakThis = + WeakReference(this); + + // Callback method used to implement the behavior described above: + void onChanged(Object stateAsObject) { + // This cast is safe because the Observer implementation ensures + // the type of stateAsObject is the same as the observer this callback + // is attached to. + final CameraState state = stateAsObject as CameraState; + if (state.type == CameraStateType.closing) { + weakThis.target!.cameraEventStreamController + .add(CameraClosingEvent(cameraId)); + } + if (state.error != null) { + weakThis.target!.cameraEventStreamController + .add(CameraErrorEvent(cameraId, state.error!.getDescription())); + } + } + + // shouldCreateDetachedObjectForTesting is used to create an Observer + // detached from the native side only to test the logic of the Analyzer + // instance that will be used for image streaming. + return _shouldCreateDetachedObjectForTesting + ? Observer.detached(onChanged: onChanged) + : Observer(onChanged: onChanged); + } + // Methods for mapping Flutter camera constants to CameraX constants: /// Returns [CameraSelector] lens direction that maps to specified @@ -546,6 +822,18 @@ class AndroidCameraCameraX extends CameraPlatform { targetFlashMode: flashMode, targetResolution: targetResolution); } + /// Returns a [Recorder] for use in video capture. + @visibleForTesting + Recorder createRecorder() { + return Recorder(); + } + + /// Returns a [VideoCapture] associated with the provided [Recorder]. + @visibleForTesting + Future createVideoCapture(Recorder recorder) async { + return VideoCapture.withOutput(recorder); + } + /// Returns an [ImageAnalysis] configured with specified target resolution. @visibleForTesting ImageAnalysis createImageAnalysis(ResolutionInfo? 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 493071ec852a..b268323ccfe2 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 @@ -6,38 +6,79 @@ import 'analyzer.dart'; import 'camera.dart'; import 'camera_info.dart'; import 'camera_selector.dart'; +import 'camera_state.dart'; +import 'camera_state_error.dart'; import 'camerax_library.g.dart'; +import 'exposure_state.dart'; import 'image_proxy.dart'; import 'java_object.dart'; +import 'live_data.dart'; +import 'observer.dart'; +import 'pending_recording.dart'; import 'plane_proxy.dart'; import 'process_camera_provider.dart'; +import 'recorder.dart'; +import 'recording.dart'; import 'system_services.dart'; +import 'video_capture.dart'; +import 'zoom_state.dart'; /// Handles initialization of Flutter APIs for the Android CameraX library. class AndroidCameraXCameraFlutterApis { /// Creates a [AndroidCameraXCameraFlutterApis]. AndroidCameraXCameraFlutterApis({ - JavaObjectFlutterApiImpl? javaObjectFlutterApi, - CameraFlutterApiImpl? cameraFlutterApi, - CameraInfoFlutterApiImpl? cameraInfoFlutterApi, - CameraSelectorFlutterApiImpl? cameraSelectorFlutterApi, - ProcessCameraProviderFlutterApiImpl? processCameraProviderFlutterApi, - SystemServicesFlutterApiImpl? systemServicesFlutterApi, - AnalyzerFlutterApiImpl? analyzerFlutterApiImpl, + JavaObjectFlutterApiImpl? javaObjectFlutterApiImpl, + CameraFlutterApiImpl? cameraFlutterApiImpl, + CameraInfoFlutterApiImpl? cameraInfoFlutterApiImpl, + CameraSelectorFlutterApiImpl? cameraSelectorFlutterApiImpl, + ProcessCameraProviderFlutterApiImpl? processCameraProviderFlutterApiImpl, + SystemServicesFlutterApiImpl? systemServicesFlutterApiImpl, + CameraStateErrorFlutterApiImpl? cameraStateErrorFlutterApiImpl, + CameraStateFlutterApiImpl? cameraStateFlutterApiImpl, + PendingRecordingFlutterApiImpl? pendingRecordingFlutterApiImpl, + RecordingFlutterApiImpl? recordingFlutterApiImpl, + RecorderFlutterApiImpl? recorderFlutterApiImpl, + VideoCaptureFlutterApiImpl? videoCaptureFlutterApiImpl, + ExposureStateFlutterApiImpl? exposureStateFlutterApiImpl, + ZoomStateFlutterApiImpl? zoomStateFlutterApiImpl, + LiveDataFlutterApiImpl? liveDataFlutterApiImpl, + ObserverFlutterApiImpl? observerFlutterApiImpl, ImageProxyFlutterApiImpl? imageProxyFlutterApiImpl, PlaneProxyFlutterApiImpl? planeProxyFlutterApiImpl, + AnalyzerFlutterApiImpl? analyzerFlutterApiImpl, }) { - this.javaObjectFlutterApi = - javaObjectFlutterApi ?? JavaObjectFlutterApiImpl(); - this.cameraInfoFlutterApi = - cameraInfoFlutterApi ?? CameraInfoFlutterApiImpl(); - this.cameraSelectorFlutterApi = - cameraSelectorFlutterApi ?? CameraSelectorFlutterApiImpl(); - this.processCameraProviderFlutterApi = processCameraProviderFlutterApi ?? - ProcessCameraProviderFlutterApiImpl(); - this.cameraFlutterApi = cameraFlutterApi ?? CameraFlutterApiImpl(); - this.systemServicesFlutterApi = - systemServicesFlutterApi ?? SystemServicesFlutterApiImpl(); + this.javaObjectFlutterApiImpl = + javaObjectFlutterApiImpl ?? JavaObjectFlutterApiImpl(); + this.cameraInfoFlutterApiImpl = + cameraInfoFlutterApiImpl ?? CameraInfoFlutterApiImpl(); + this.cameraSelectorFlutterApiImpl = + cameraSelectorFlutterApiImpl ?? CameraSelectorFlutterApiImpl(); + this.processCameraProviderFlutterApiImpl = + processCameraProviderFlutterApiImpl ?? + ProcessCameraProviderFlutterApiImpl(); + this.cameraFlutterApiImpl = cameraFlutterApiImpl ?? CameraFlutterApiImpl(); + this.systemServicesFlutterApiImpl = + systemServicesFlutterApiImpl ?? SystemServicesFlutterApiImpl(); + this.cameraStateErrorFlutterApiImpl = + cameraStateErrorFlutterApiImpl ?? CameraStateErrorFlutterApiImpl(); + this.cameraStateFlutterApiImpl = + cameraStateFlutterApiImpl ?? CameraStateFlutterApiImpl(); + this.pendingRecordingFlutterApiImpl = + pendingRecordingFlutterApiImpl ?? PendingRecordingFlutterApiImpl(); + this.recordingFlutterApiImpl = + recordingFlutterApiImpl ?? RecordingFlutterApiImpl(); + this.recorderFlutterApiImpl = + recorderFlutterApiImpl ?? RecorderFlutterApiImpl(); + this.videoCaptureFlutterApiImpl = + videoCaptureFlutterApiImpl ?? VideoCaptureFlutterApiImpl(); + this.exposureStateFlutterApiImpl = + exposureStateFlutterApiImpl ?? ExposureStateFlutterApiImpl(); + this.zoomStateFlutterApiImpl = + zoomStateFlutterApiImpl ?? ZoomStateFlutterApiImpl(); + this.liveDataFlutterApiImpl = + liveDataFlutterApiImpl ?? LiveDataFlutterApiImpl(); + this.observerFlutterApiImpl = + observerFlutterApiImpl ?? ObserverFlutterApiImpl(); this.analyzerFlutterApiImpl = analyzerFlutterApiImpl ?? AnalyzerFlutterApiImpl(); this.imageProxyFlutterApiImpl = @@ -55,23 +96,53 @@ class AndroidCameraXCameraFlutterApis { AndroidCameraXCameraFlutterApis(); /// Handles callbacks methods for the native Java Object class. - late final JavaObjectFlutterApi javaObjectFlutterApi; + late final JavaObjectFlutterApi javaObjectFlutterApiImpl; - /// Flutter Api for [CameraInfo]. - late final CameraInfoFlutterApiImpl cameraInfoFlutterApi; + /// Flutter Api implementation for [CameraInfo]. + late final CameraInfoFlutterApiImpl cameraInfoFlutterApiImpl; - /// Flutter Api for [CameraSelector]. - late final CameraSelectorFlutterApiImpl cameraSelectorFlutterApi; + /// Flutter Api implementation for [CameraSelector]. + late final CameraSelectorFlutterApiImpl cameraSelectorFlutterApiImpl; - /// Flutter Api for [ProcessCameraProvider]. + /// Flutter Api implementation for [ProcessCameraProvider]. late final ProcessCameraProviderFlutterApiImpl - processCameraProviderFlutterApi; + processCameraProviderFlutterApiImpl; + + /// Flutter Api implementation for [Camera]. + late final CameraFlutterApiImpl cameraFlutterApiImpl; + + /// Flutter Api implementation for [SystemServices]. + late final SystemServicesFlutterApiImpl systemServicesFlutterApiImpl; + + /// Flutter Api implementation for [CameraStateError]. + late final CameraStateErrorFlutterApiImpl? cameraStateErrorFlutterApiImpl; + + /// Flutter Api implementation for [CameraState]. + late final CameraStateFlutterApiImpl? cameraStateFlutterApiImpl; + + /// Flutter Api implementation for [LiveData]. + late final LiveDataFlutterApiImpl? liveDataFlutterApiImpl; + + /// Flutter Api implementation for [Observer]. + late final ObserverFlutterApiImpl? observerFlutterApiImpl; + + /// Flutter Api for [PendingRecording]. + late final PendingRecordingFlutterApiImpl pendingRecordingFlutterApiImpl; + + /// Flutter Api for [Recording]. + late final RecordingFlutterApiImpl recordingFlutterApiImpl; + + /// Flutter Api for [Recorder]. + late final RecorderFlutterApiImpl recorderFlutterApiImpl; + + /// Flutter Api for [VideoCapture]. + late final VideoCaptureFlutterApiImpl videoCaptureFlutterApiImpl; - /// Flutter Api for [Camera]. - late final CameraFlutterApiImpl cameraFlutterApi; + /// Flutter Api for [ExposureState]. + late final ExposureStateFlutterApiImpl exposureStateFlutterApiImpl; - /// Flutter Api for [SystemServices]. - late final SystemServicesFlutterApiImpl systemServicesFlutterApi; + /// Flutter Api for [ZoomState]. + late final ZoomStateFlutterApiImpl zoomStateFlutterApiImpl; /// Flutter Api implementation for [Analyzer]. late final AnalyzerFlutterApiImpl analyzerFlutterApiImpl; @@ -85,15 +156,26 @@ class AndroidCameraXCameraFlutterApis { /// Ensures all the Flutter APIs have been setup to receive calls from native code. void ensureSetUp() { if (!_haveBeenSetUp) { - JavaObjectFlutterApi.setup(javaObjectFlutterApi); - CameraInfoFlutterApi.setup(cameraInfoFlutterApi); - CameraSelectorFlutterApi.setup(cameraSelectorFlutterApi); - ProcessCameraProviderFlutterApi.setup(processCameraProviderFlutterApi); - CameraFlutterApi.setup(cameraFlutterApi); - SystemServicesFlutterApi.setup(systemServicesFlutterApi); + JavaObjectFlutterApi.setup(javaObjectFlutterApiImpl); + CameraInfoFlutterApi.setup(cameraInfoFlutterApiImpl); + CameraSelectorFlutterApi.setup(cameraSelectorFlutterApiImpl); + ProcessCameraProviderFlutterApi.setup( + processCameraProviderFlutterApiImpl); + CameraFlutterApi.setup(cameraFlutterApiImpl); + SystemServicesFlutterApi.setup(systemServicesFlutterApiImpl); + CameraStateErrorFlutterApi.setup(cameraStateErrorFlutterApiImpl); + CameraStateFlutterApi.setup(cameraStateFlutterApiImpl); + PendingRecordingFlutterApi.setup(pendingRecordingFlutterApiImpl); + RecordingFlutterApi.setup(recordingFlutterApiImpl); + RecorderFlutterApi.setup(recorderFlutterApiImpl); + VideoCaptureFlutterApi.setup(videoCaptureFlutterApiImpl); + ExposureStateFlutterApi.setup(exposureStateFlutterApiImpl); + ZoomStateFlutterApi.setup(zoomStateFlutterApiImpl); AnalyzerFlutterApi.setup(analyzerFlutterApiImpl); ImageProxyFlutterApi.setup(imageProxyFlutterApiImpl); PlaneProxyFlutterApi.setup(planeProxyFlutterApiImpl); + LiveDataFlutterApi.setup(liveDataFlutterApiImpl); + ObserverFlutterApi.setup(observerFlutterApiImpl); _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 51e0813cdd32..8720fdf322f1 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart' show BinaryMessenger; import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camera_info.dart'; import 'camerax_library.g.dart'; import 'instance_manager.dart'; import 'java_object.dart'; @@ -15,41 +16,87 @@ import 'java_object.dart'; /// See https://developer.android.com/reference/androidx/camera/core/Camera. class Camera extends JavaObject { /// Constructs a [Camera] that is not automatically attached to a native object. - Camera.detached({super.binaryMessenger, super.instanceManager}) - : super.detached() { + Camera.detached( + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = CameraHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); } + + late final CameraHostApiImpl _api; + + /// Retrieve the [CameraInfo] instance that contains information about this + /// instance. + Future getCameraInfo() async { + return _api.getCameraInfoFromInstance(this); + } +} + +/// Host API implementation of [Camera]. +class CameraHostApiImpl extends CameraHostApi { + /// Constructs a [CameraHostApiImpl]. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. + CameraHostApiImpl({this.binaryMessenger, InstanceManager? instanceManager}) + : super(binaryMessenger: binaryMessenger) { + this.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. + late final InstanceManager instanceManager; + + /// Gets the [CameraInfo] associated with the specified instance of [Camera]. + Future getCameraInfoFromInstance(Camera instance) async { + final int identifier = instanceManager.getIdentifier(instance)!; + final int cameraInfoId = await getCameraInfo(identifier); + + return instanceManager + .getInstanceWithWeakReference(cameraInfoId)!; + } } /// Flutter API implementation of [Camera]. class CameraFlutterApiImpl implements CameraFlutterApi { /// Constructs a [CameraFlutterApiImpl]. /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. + /// /// An [instanceManager] is typically passed when a copy of an instance - /// contained by an `InstanceManager` is being created. + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. CameraFlutterApiImpl({ - this.binaryMessenger, + BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + }) : _binaryMessenger = binaryMessenger, + _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; + final BinaryMessenger? _binaryMessenger; /// Maintains instances stored to communicate with native language objects. - final InstanceManager instanceManager; + final InstanceManager _instanceManager; @override void create(int identifier) { - instanceManager.addHostCreatedInstance( + _instanceManager.addHostCreatedInstance( Camera.detached( - binaryMessenger: binaryMessenger, instanceManager: instanceManager), + binaryMessenger: _binaryMessenger, instanceManager: _instanceManager), identifier, onCopy: (Camera original) { return Camera.detached( - binaryMessenger: binaryMessenger, instanceManager: instanceManager); + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager); }, ); } diff --git a/packages/camera/camera_android_camerax/lib/src/camera_info.dart b/packages/camera/camera_android_camerax/lib/src/camera_info.dart index 8c2c7bcf0aec..4ed78ddfbf37 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera_info.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera_info.dart @@ -2,14 +2,20 @@ // 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 'android_camera_camerax_flutter_api_impls.dart'; +import 'camera_state.dart'; import 'camerax_library.g.dart'; +import 'exposure_state.dart'; import 'instance_manager.dart'; import 'java_object.dart'; +import 'live_data.dart'; +import 'zoom_state.dart'; -/// Represents the metadata of a camera. +/// The metadata of a camera. /// /// See https://developer.android.com/reference/androidx/camera/core/CameraInfo. class CameraInfo extends JavaObject { @@ -19,22 +25,34 @@ class CameraInfo extends JavaObject { : super.detached( binaryMessenger: binaryMessenger, instanceManager: instanceManager) { - _api = CameraInfoHostApiImpl( + _api = _CameraInfoHostApiImpl( binaryMessenger: binaryMessenger, instanceManager: instanceManager); AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); } - late final CameraInfoHostApiImpl _api; + late final _CameraInfoHostApiImpl _api; - /// Gets sensor orientation degrees of camera. + /// Gets sensor orientation degrees of the camera. Future getSensorRotationDegrees() => _api.getSensorRotationDegreesFromInstance(this); + + /// Starts listening for the camera closing. + Future> getCameraState() => + _api.getCameraStateFromInstance(this); + + /// Gets the exposure state of the camera. + Future getExposureState() => + _api.getExposureStateFromInstance(this); + + /// Gets the live zoom state of the camera. + Future> getZoomState() => + _api.getZoomStateFromInstance(this); } /// Host API implementation of [CameraInfo]. -class CameraInfoHostApiImpl extends CameraInfoHostApi { - /// Constructs a [CameraInfoHostApiImpl]. - CameraInfoHostApiImpl( +class _CameraInfoHostApiImpl extends CameraInfoHostApi { + /// Constructs a [_CameraInfoHostApiImpl]. + _CameraInfoHostApiImpl( {super.binaryMessenger, InstanceManager? instanceManager}) { this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager; } @@ -42,7 +60,7 @@ class CameraInfoHostApiImpl extends CameraInfoHostApi { /// Maintains instances stored to communicate with native language objects. late final InstanceManager instanceManager; - /// Gets sensor orientation degrees of [CameraInfo]. + /// Gets sensor orientation degrees of the specified [CameraInfo] instance. Future getSensorRotationDegreesFromInstance( CameraInfo instance, ) async { @@ -50,34 +68,70 @@ class CameraInfoHostApiImpl extends CameraInfoHostApi { instanceManager.getIdentifier(instance)!); return sensorRotationDegrees; } + + /// Gets the [LiveData] that represents the state of the camera + /// to which the CameraInfo [instance] pertains. + Future> getCameraStateFromInstance( + CameraInfo instance) async { + final int? identifier = instanceManager.getIdentifier(instance); + final int liveCameraStateId = await getCameraState(identifier!); + final LiveData liveCameraState = + instanceManager.getInstanceWithWeakReference>( + liveCameraStateId)!; + return liveCameraState; + } + + /// Gets the [ExposureState] of the specified [CameraInfo] instance. + Future getExposureStateFromInstance( + CameraInfo instance) async { + final int? identifier = instanceManager.getIdentifier(instance); + final int exposureStateIdentifier = await getExposureState(identifier!); + return instanceManager + .getInstanceWithWeakReference(exposureStateIdentifier)!; + } + + /// Gets the [LiveData] of the specified [CameraInfo] instance. + Future> getZoomStateFromInstance( + CameraInfo instance) async { + final int? identifier = instanceManager.getIdentifier(instance); + final int zoomStateIdentifier = await getZoomState(identifier!); + return instanceManager.getInstanceWithWeakReference>( + zoomStateIdentifier)!; + } } /// Flutter API implementation of [CameraInfo]. class CameraInfoFlutterApiImpl extends CameraInfoFlutterApi { /// Constructs a [CameraInfoFlutterApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. CameraInfoFlutterApiImpl({ - this.binaryMessenger, + BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + }) : _binaryMessenger = binaryMessenger, + _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; + final BinaryMessenger? _binaryMessenger; /// Maintains instances stored to communicate with native language objects. - final InstanceManager instanceManager; + final InstanceManager _instanceManager; @override void create(int identifier) { - instanceManager.addHostCreatedInstance( + _instanceManager.addHostCreatedInstance( CameraInfo.detached( - binaryMessenger: binaryMessenger, instanceManager: instanceManager), + binaryMessenger: _binaryMessenger, instanceManager: _instanceManager), identifier, onCopy: (CameraInfo original) { return CameraInfo.detached( - binaryMessenger: binaryMessenger, instanceManager: instanceManager); + binaryMessenger: _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 8300063204c0..1df886b861bd 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera_selector.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera_selector.dart @@ -163,32 +163,37 @@ class CameraSelectorHostApiImpl extends CameraSelectorHostApi { /// Flutter API implementation of [CameraSelector]. class CameraSelectorFlutterApiImpl implements CameraSelectorFlutterApi { /// Constructs a [CameraSelectorFlutterApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. CameraSelectorFlutterApiImpl({ - this.binaryMessenger, + BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + }) : _binaryMessenger = binaryMessenger, + _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; + final BinaryMessenger? _binaryMessenger; /// Maintains instances stored to communicate with native language objects. - final InstanceManager instanceManager; + final InstanceManager _instanceManager; @override void create(int identifier, int? lensFacing) { - instanceManager.addHostCreatedInstance( + _instanceManager.addHostCreatedInstance( CameraSelector.detached( - binaryMessenger: binaryMessenger, - instanceManager: instanceManager, + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, lensFacing: lensFacing), identifier, onCopy: (CameraSelector original) { return CameraSelector.detached( - binaryMessenger: binaryMessenger, - instanceManager: instanceManager, + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, lensFacing: original.lensFacing); }, ); diff --git a/packages/camera/camera_android_camerax/lib/src/camera_state.dart b/packages/camera/camera_android_camerax/lib/src/camera_state.dart new file mode 100644 index 000000000000..c3dc25a46895 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/camera_state.dart @@ -0,0 +1,126 @@ +// 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/services.dart'; +import 'package:meta/meta.dart'; + +import 'camera_state_error.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// A snapshot of the camera state. +/// +/// See https://developer.android.com/reference/androidx/camera/core/CameraState. +class CameraState extends JavaObject { + /// Constructs a [CameraState] that is not automatically attached to a native object. + CameraState.detached( + {super.binaryMessenger, + super.instanceManager, + required this.type, + this.error}) + : super.detached(); + + /// The type of state that the camera is in. + final CameraStateType type; + + /// The error that the camera has encountered, if any. + final CameraStateError? error; + + /// Error code indicating that the camera device is already in use. + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraState#ERROR_CAMERA_IN_USE() + static const int errorCameraInUse = 1; + + /// Error code indicating that the limit number of open cameras has been + /// reached. + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraState#ERROR_MAX_CAMERAS_IN_USE() + static const int errorMaxCamerasInUse = 2; + + /// Error code indicating that the camera device has encountered a recoverable + /// error. + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraState#ERROR_OTHER_RECOVERABLE_ERROR() + static const int errorOtherRecoverableError = 3; + + /// Error code inidcating that configuring the camera has failed. + /// + /// https://developer.android.com/reference/androidx/camera/core/CameraState#ERROR_STREAM_CONFIG() + static const int errorStreamConfig = 4; + + /// Error code indicating that the camera device could not be opened due to a + /// device policy. + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraState#ERROR_CAMERA_DISABLED() + static const int errorCameraDisabled = 5; + + /// Error code indicating that the camera device was closed due to a fatal + /// error. + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraState#ERROR_CAMERA_FATAL_ERROR() + static const int errorCameraFatalError = 6; + + /// Error code indicating that the camera could not be opened because + /// "Do Not Disturb" mode is enabled on devices affected by a bug in Android 9 + /// (API level 28). + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraState#ERROR_DO_NOT_DISTURB_MODE_ENABLED() + static const int errorDoNotDisturbModeEnabled = 7; +} + +/// Flutter API implementation for [CameraState]. +/// +/// 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 CameraStateFlutterApiImpl implements CameraStateFlutterApi { + /// Constructs a [CameraStateFlutterApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. + CameraStateFlutterApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _binaryMessenger = binaryMessenger, + _instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + final BinaryMessenger? _binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager _instanceManager; + + @override + void create( + int identifier, + CameraStateTypeData type, + int? errorIdentifier, + ) { + _instanceManager.addHostCreatedInstance( + CameraState.detached( + type: type.value, + error: errorIdentifier == null + ? null + : _instanceManager.getInstanceWithWeakReference( + errorIdentifier, + ), + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, + ), + identifier, + onCopy: (CameraState original) => CameraState.detached( + type: original.type, + error: original.error, + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, + ), + ); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/camera_state_error.dart b/packages/camera/camera_android_camerax/lib/src/camera_state_error.dart new file mode 100644 index 000000000000..89fb0f8ac176 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/camera_state_error.dart @@ -0,0 +1,122 @@ +// 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/services.dart'; +import 'package:meta/meta.dart'; + +import 'camera_state.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// The error that a camera has encountered. +/// +/// See https://developer.android.com/reference/androidx/camera/core/CameraState.StateError. +class CameraStateError extends JavaObject { + /// Constructs a [CameraStateError] that is not automatically attached to a native object. + CameraStateError.detached( + {super.binaryMessenger, super.instanceManager, required this.code}) + : super.detached(); + + /// The code of this error. + /// + /// Will map to one of the [CameraState] error codes that map to the CameraX + /// CameraState codes: + /// https://developer.android.com/reference/androidx/camera/core/CameraState#constants_1. + final int code; + + /// Gets a description of this error corresponding to its [code]. + /// + /// This is not directly provided by the CameraX library, but is determined + /// based on the description of the [code]. + /// + /// Provided for developers to use for error handling. + String getDescription() { + String description = ''; + switch (code) { + case CameraState.errorCameraInUse: + description = + 'The camera was already in use, possibly by a higher-priority camera client.'; + break; + case CameraState.errorMaxCamerasInUse: + description = + 'The limit number of open cameras has been reached, and more cameras cannot be opened until other instances are closed.'; + break; + case CameraState.errorOtherRecoverableError: + description = + 'The camera device has encountered a recoverable error. CameraX will attempt to recover from the error.'; + break; + case CameraState.errorStreamConfig: + description = 'Configuring the camera has failed.'; + break; + case CameraState.errorCameraDisabled: + description = + 'The camera device could not be opened due to a device policy. Thia may be caused by a client from a background process attempting to open the camera.'; + break; + case CameraState.errorCameraFatalError: + description = + 'The camera was closed due to a fatal error. This may require the Android device be shut down and restarted to restore camera function or may indicate a persistent camera hardware problem.'; + break; + case CameraState.errorDoNotDisturbModeEnabled: + description = + 'The camera could not be opened because "Do Not Disturb" mode is enabled. Please disable this mode, and try opening the camera again.'; + break; + default: + description = + 'There was an unspecified issue with the current camera state.'; + break; + } + + return '$code : $description'; + } +} + +/// Flutter API implementation for [CameraStateError]. +/// +/// 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 CameraStateErrorFlutterApiImpl implements CameraStateErrorFlutterApi { + /// Constructs a [CameraStateErrorFlutterApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. If left null, it + /// will default to the global instance defined in [JavaObject]. + CameraStateErrorFlutterApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _binaryMessenger = binaryMessenger, + _instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + final BinaryMessenger? _binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager _instanceManager; + + @override + void create( + int identifier, + int code, + ) { + _instanceManager.addHostCreatedInstance( + CameraStateError.detached( + code: code, + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, + ), + identifier, + onCopy: (CameraStateError original) => CameraStateError.detached( + code: original.code, + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, + ), + ); + } +} 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 5e8e95548d38..2a1b2813f1aa 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.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 @@ -11,6 +11,22 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +/// The states the camera can be in. +/// +/// See https://developer.android.com/reference/androidx/camera/core/CameraState.Type. +enum CameraStateType { + closed, + closing, + open, + opening, + pendingOpen, +} + +enum LiveDataSupportedType { + cameraState, + zoomState, +} + class ResolutionInfo { ResolutionInfo({ required this.width, @@ -63,6 +79,74 @@ class CameraPermissionsErrorData { } } +class CameraStateTypeData { + CameraStateTypeData({ + required this.value, + }); + + CameraStateType value; + + Object encode() { + return [ + value.index, + ]; + } + + static CameraStateTypeData decode(Object result) { + result as List; + return CameraStateTypeData( + value: CameraStateType.values[result[0]! as int], + ); + } +} + +class LiveDataSupportedTypeData { + LiveDataSupportedTypeData({ + required this.value, + }); + + LiveDataSupportedType value; + + Object encode() { + return [ + value.index, + ]; + } + + static LiveDataSupportedTypeData decode(Object result) { + result as List; + return LiveDataSupportedTypeData( + value: LiveDataSupportedType.values[result[0]! as int], + ); + } +} + +class ExposureCompensationRange { + ExposureCompensationRange({ + required this.minCompensation, + required this.maxCompensation, + }); + + int minCompensation; + + int maxCompensation; + + Object encode() { + return [ + minCompensation, + maxCompensation, + ]; + } + + static ExposureCompensationRange decode(Object result) { + result as List; + return ExposureCompensationRange( + minCompensation: result[0]! as int, + maxCompensation: result[1]! as int, + ); + } +} + class InstanceManagerHostApi { /// Constructor for [InstanceManagerHostApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default @@ -196,6 +280,87 @@ class CameraInfoHostApi { return (replyList[0] as int?)!; } } + + Future getCameraState(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraInfoHostApi.getCameraState', 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 int?)!; + } + } + + Future getExposureState(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraInfoHostApi.getExposureState', 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 int?)!; + } + } + + Future getZoomState(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraInfoHostApi.getZoomState', 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 int?)!; + } + } } abstract class CameraInfoFlutterApi { @@ -516,6 +681,44 @@ abstract class ProcessCameraProviderFlutterApi { } } +class CameraHostApi { + /// Constructor for [CameraHostApi]. 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. + CameraHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future getCameraInfo(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraHostApi.getCameraInfo', 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 int?)!; + } + } +} + abstract class CameraFlutterApi { static const MessageCodec codec = StandardMessageCodec(); @@ -647,6 +850,33 @@ class SystemServicesHostApi { return; } } + + Future getTempFilePath(String arg_prefix, String arg_suffix) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_prefix, arg_suffix]) 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?)!; + } + } } abstract class SystemServicesFlutterApi { @@ -838,47 +1068,22 @@ class PreviewHostApi { } } -class _ImageCaptureHostApiCodec extends StandardMessageCodec { - const _ImageCaptureHostApiCodec(); - @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 ImageCaptureHostApi { - /// Constructor for [ImageCaptureHostApi]. The [binaryMessenger] named argument is +class VideoCaptureHostApi { + /// Constructor for [VideoCaptureHostApi]. 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. - ImageCaptureHostApi({BinaryMessenger? binaryMessenger}) + VideoCaptureHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _ImageCaptureHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); - Future create(int arg_identifier, int? arg_flashMode, - ResolutionInfo? arg_targetResolution) async { + Future withOutput(int arg_videoOutputId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ImageCaptureHostApi.create', codec, + 'dev.flutter.pigeon.VideoCaptureHostApi.withOutput', codec, binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send( - [arg_identifier, arg_flashMode, arg_targetResolution]) - as List?; + final List? replyList = + await channel.send([arg_videoOutputId]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -890,36 +1095,19 @@ class ImageCaptureHostApi { message: replyList[1] as String?, details: replyList[2], ); - } else { - return; - } - } - - Future setFlashMode(int arg_identifier, int arg_flashMode) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ImageCaptureHostApi.setFlashMode', codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel - .send([arg_identifier, arg_flashMode]) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { + } else if (replyList[0] == null) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', ); } else { - return; + return (replyList[0] as int?)!; } } - Future takePicture(int arg_identifier) async { + Future getOutput(int arg_identifier) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ImageCaptureHostApi.takePicture', codec, + 'dev.flutter.pigeon.VideoCaptureHostApi.getOutput', codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_identifier]) as List?; @@ -940,27 +1128,661 @@ class ImageCaptureHostApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as String?)!; + return (replyList[0] as int?)!; } } } -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); - } - } +abstract class VideoCaptureFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 128: + void create(int identifier); + + static void setup(VideoCaptureFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoCaptureFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoCaptureFlutterApi.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.VideoCaptureFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + +class RecorderHostApi { + /// Constructor for [RecorderHostApi]. 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. + RecorderHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future create( + int arg_identifier, int? arg_aspectRatio, int? arg_bitRate) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_aspectRatio, arg_bitRate]) + 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 getAspectRatio(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.getAspectRatio', 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 int?)!; + } + } + + Future getTargetVideoEncodingBitRate(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.getTargetVideoEncodingBitRate', + 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 int?)!; + } + } + + Future prepareRecording(int arg_identifier, String arg_path) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.prepareRecording', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_path]) 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 int?)!; + } + } +} + +abstract class RecorderFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier, int? aspectRatio, int? bitRate); + + static void setup(RecorderFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.RecorderFlutterApi.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.RecorderFlutterApi.create was null, expected non-null int.'); + final int? arg_aspectRatio = (args[1] as int?); + final int? arg_bitRate = (args[2] as int?); + api.create(arg_identifier!, arg_aspectRatio, arg_bitRate); + return; + }); + } + } + } +} + +class PendingRecordingHostApi { + /// Constructor for [PendingRecordingHostApi]. 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. + PendingRecordingHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future start(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PendingRecordingHostApi.start', 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 int?)!; + } + } +} + +abstract class PendingRecordingFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier); + + static void setup(PendingRecordingFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PendingRecordingFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PendingRecordingFlutterApi.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.PendingRecordingFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + +class RecordingHostApi { + /// Constructor for [RecordingHostApi]. 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. + RecordingHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future close(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.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; + } + } + + Future pause(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.pause', 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; + } + } + + Future resume(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.resume', 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; + } + } + + Future stop(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.stop', 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 RecordingFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier); + + static void setup(RecordingFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.RecordingFlutterApi.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.RecordingFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + +class _ImageCaptureHostApiCodec extends StandardMessageCodec { + const _ImageCaptureHostApiCodec(); + @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 ImageCaptureHostApi { + /// Constructor for [ImageCaptureHostApi]. 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. + ImageCaptureHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _ImageCaptureHostApiCodec(); + + Future create(int arg_identifier, int? arg_flashMode, + ResolutionInfo? arg_targetResolution) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageCaptureHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send( + [arg_identifier, arg_flashMode, arg_targetResolution]) + 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 setFlashMode(int arg_identifier, int arg_flashMode) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageCaptureHostApi.setFlashMode', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_flashMode]) 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 takePicture(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageCaptureHostApi.takePicture', 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 String?)!; + } + } +} + +class _CameraStateFlutterApiCodec extends StandardMessageCodec { + const _CameraStateFlutterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CameraStateTypeData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CameraStateTypeData.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class CameraStateFlutterApi { + static const MessageCodec codec = _CameraStateFlutterApiCodec(); + + void create(int identifier, CameraStateTypeData type, int? errorIdentifier); + + static void setup(CameraStateFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraStateFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraStateFlutterApi.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.CameraStateFlutterApi.create was null, expected non-null int.'); + final CameraStateTypeData? arg_type = + (args[1] as CameraStateTypeData?); + assert(arg_type != null, + 'Argument for dev.flutter.pigeon.CameraStateFlutterApi.create was null, expected non-null CameraStateTypeData.'); + final int? arg_errorIdentifier = (args[2] as int?); + api.create(arg_identifier!, arg_type!, arg_errorIdentifier); + return; + }); + } + } + } +} + +class _ExposureStateFlutterApiCodec extends StandardMessageCodec { + const _ExposureStateFlutterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is ExposureCompensationRange) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return ExposureCompensationRange.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class ExposureStateFlutterApi { + static const MessageCodec codec = _ExposureStateFlutterApiCodec(); + + void create( + int identifier, + ExposureCompensationRange exposureCompensationRange, + double exposureCompensationStep); + + static void setup(ExposureStateFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ExposureStateFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ExposureStateFlutterApi.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.ExposureStateFlutterApi.create was null, expected non-null int.'); + final ExposureCompensationRange? arg_exposureCompensationRange = + (args[1] as ExposureCompensationRange?); + assert(arg_exposureCompensationRange != null, + 'Argument for dev.flutter.pigeon.ExposureStateFlutterApi.create was null, expected non-null ExposureCompensationRange.'); + final double? arg_exposureCompensationStep = (args[2] as double?); + assert(arg_exposureCompensationStep != null, + 'Argument for dev.flutter.pigeon.ExposureStateFlutterApi.create was null, expected non-null double.'); + api.create(arg_identifier!, arg_exposureCompensationRange!, + arg_exposureCompensationStep!); + return; + }); + } + } + } +} + +abstract class ZoomStateFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier, double minZoomRatio, double maxZoomRatio); + + static void setup(ZoomStateFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ZoomStateFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ZoomStateFlutterApi.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.ZoomStateFlutterApi.create was null, expected non-null int.'); + final double? arg_minZoomRatio = (args[1] as double?); + assert(arg_minZoomRatio != null, + 'Argument for dev.flutter.pigeon.ZoomStateFlutterApi.create was null, expected non-null double.'); + final double? arg_maxZoomRatio = (args[2] as double?); + assert(arg_maxZoomRatio != null, + 'Argument for dev.flutter.pigeon.ZoomStateFlutterApi.create was null, expected non-null double.'); + api.create(arg_identifier!, arg_minZoomRatio!, arg_maxZoomRatio!); + return; + }); + } + } + } +} + +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); @@ -1082,6 +1904,261 @@ class AnalyzerHostApi { } } +class ObserverHostApi { + /// Constructor for [ObserverHostApi]. 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. + ObserverHostApi({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.ObserverHostApi.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 ObserverFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + void onChanged(int identifier, int valueIdentifier); + + static void setup(ObserverFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ObserverFlutterApi.onChanged', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ObserverFlutterApi.onChanged 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.ObserverFlutterApi.onChanged was null, expected non-null int.'); + final int? arg_valueIdentifier = (args[1] as int?); + assert(arg_valueIdentifier != null, + 'Argument for dev.flutter.pigeon.ObserverFlutterApi.onChanged was null, expected non-null int.'); + api.onChanged(arg_identifier!, arg_valueIdentifier!); + return; + }); + } + } + } +} + +abstract class CameraStateErrorFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier, int code); + + static void setup(CameraStateErrorFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraStateErrorFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraStateErrorFlutterApi.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.CameraStateErrorFlutterApi.create was null, expected non-null int.'); + final int? arg_code = (args[1] as int?); + assert(arg_code != null, + 'Argument for dev.flutter.pigeon.CameraStateErrorFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!, arg_code!); + return; + }); + } + } + } +} + +class _LiveDataHostApiCodec extends StandardMessageCodec { + const _LiveDataHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is LiveDataSupportedTypeData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return LiveDataSupportedTypeData.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class LiveDataHostApi { + /// Constructor for [LiveDataHostApi]. 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. + LiveDataHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _LiveDataHostApiCodec(); + + Future observe(int arg_identifier, int arg_observerIdentifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LiveDataHostApi.observe', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier, arg_observerIdentifier]) + 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 removeObservers(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LiveDataHostApi.removeObservers', 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; + } + } + + Future getValue( + int arg_identifier, LiveDataSupportedTypeData arg_type) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LiveDataHostApi.getValue', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_type]) 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 (replyList[0] as int?); + } + } +} + +class _LiveDataFlutterApiCodec extends StandardMessageCodec { + const _LiveDataFlutterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is LiveDataSupportedTypeData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return LiveDataSupportedTypeData.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class LiveDataFlutterApi { + static const MessageCodec codec = _LiveDataFlutterApiCodec(); + + void create(int identifier, LiveDataSupportedTypeData type); + + static void setup(LiveDataFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LiveDataFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.LiveDataFlutterApi.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.LiveDataFlutterApi.create was null, expected non-null int.'); + final LiveDataSupportedTypeData? arg_type = + (args[1] as LiveDataSupportedTypeData?); + assert(arg_type != null, + 'Argument for dev.flutter.pigeon.LiveDataFlutterApi.create was null, expected non-null LiveDataSupportedTypeData.'); + api.create(arg_identifier!, arg_type!); + return; + }); + } + } + } +} + abstract class AnalyzerFlutterApi { static const MessageCodec codec = StandardMessageCodec(); diff --git a/packages/camera/camera_android_camerax/lib/src/exposure_state.dart b/packages/camera/camera_android_camerax/lib/src/exposure_state.dart new file mode 100644 index 000000000000..4599a929995e --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/exposure_state.dart @@ -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. + +import 'package:flutter/services.dart' show BinaryMessenger; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// Represents exposure related information of a camera. +/// +/// See https://developer.android.com/reference/androidx/camera/core/ExposureState. +class ExposureState extends JavaObject { + /// Constructs a [ExposureState] that is not automatically attached to a native object. + ExposureState.detached( + {super.binaryMessenger, + super.instanceManager, + required this.exposureCompensationRange, + required this.exposureCompensationStep}) + : super.detached() { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + /// Gets the maximum and minimum exposure compensation values for the camera + /// represented by this instance. + final ExposureCompensationRange exposureCompensationRange; + + /// Gets the smallest step by which the exposure compensation can be changed for + /// the camera represented by this instance. + final double exposureCompensationStep; +} + +/// Flutter API implementation of [ExposureState]. +class ExposureStateFlutterApiImpl implements ExposureStateFlutterApi { + /// Constructs a [ExposureStateFlutterApiImpl]. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. + ExposureStateFlutterApiImpl({ + 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, + ExposureCompensationRange exposureCompensationRange, + double exposureCompensationStep) { + instanceManager.addHostCreatedInstance( + ExposureState.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + exposureCompensationRange: exposureCompensationRange, + exposureCompensationStep: exposureCompensationStep), + identifier, + onCopy: (ExposureState original) { + return ExposureState.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + exposureCompensationRange: original.exposureCompensationRange, + exposureCompensationStep: original.exposureCompensationStep); + }, + ); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/image_analysis.dart b/packages/camera/camera_android_camerax/lib/src/image_analysis.dart index 69cc7829c7a3..7ffaba72ecad 100644 --- a/packages/camera/camera_android_camerax/lib/src/image_analysis.dart +++ b/packages/camera/camera_android_camerax/lib/src/image_analysis.dart @@ -61,8 +61,12 @@ class ImageAnalysis extends UseCase { class _ImageAnalysisHostApiImpl extends ImageAnalysisHostApi { /// Constructor for [_ImageAnalysisHostApiImpl]. /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. + /// /// An [instanceManager] is typically passed when a copy of an instance - /// contained by an `InstanceManager` is being created. + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. _ImageAnalysisHostApiImpl({ this.binaryMessenger, InstanceManager? instanceManager, 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 9b80c8706214..6a9b2dedd042 100644 --- a/packages/camera/camera_android_camerax/lib/src/image_capture.dart +++ b/packages/camera/camera_android_camerax/lib/src/image_capture.dart @@ -96,8 +96,12 @@ class ImageCapture extends UseCase { class ImageCaptureHostApiImpl extends ImageCaptureHostApi { /// Constructs a [ImageCaptureHostApiImpl]. /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. + /// /// An [instanceManager] is typically passed when a copy of an instance - /// contained by an `InstanceManager` is being created. + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. ImageCaptureHostApiImpl( {this.binaryMessenger, InstanceManager? instanceManager}) { this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager; diff --git a/packages/camera/camera_android_camerax/lib/src/image_proxy.dart b/packages/camera/camera_android_camerax/lib/src/image_proxy.dart index a1f929c5960d..ded09123da6e 100644 --- a/packages/camera/camera_android_camerax/lib/src/image_proxy.dart +++ b/packages/camera/camera_android_camerax/lib/src/image_proxy.dart @@ -99,19 +99,24 @@ class _ImageProxyHostApiImpl extends ImageProxyHostApi { @protected class ImageProxyFlutterApiImpl implements ImageProxyFlutterApi { /// Constructs a [ImageProxyFlutterApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. ImageProxyFlutterApiImpl({ - this.binaryMessenger, + BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + }) : _binaryMessenger = binaryMessenger, + _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; + final BinaryMessenger? _binaryMessenger; /// Maintains instances stored to communicate with native language objects. - final InstanceManager instanceManager; + final InstanceManager _instanceManager; @override void create( @@ -120,18 +125,18 @@ class ImageProxyFlutterApiImpl implements ImageProxyFlutterApi { int height, int width, ) { - instanceManager.addHostCreatedInstance( + _instanceManager.addHostCreatedInstance( ImageProxy.detached( - binaryMessenger: binaryMessenger, - instanceManager: instanceManager, + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, format: format, height: height, width: width, ), identifier, onCopy: (ImageProxy original) => ImageProxy.detached( - binaryMessenger: binaryMessenger, - instanceManager: instanceManager, + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, format: original.format, height: original.height, width: original.width), diff --git a/packages/camera/camera_android_camerax/lib/src/instance_manager.dart b/packages/camera/camera_android_camerax/lib/src/instance_manager.dart index ac9856a63670..aeb97ec36faa 100644 --- a/packages/camera/camera_android_camerax/lib/src/instance_manager.dart +++ b/packages/camera/camera_android_camerax/lib/src/instance_manager.dart @@ -123,12 +123,12 @@ class InstanceManager { if (weakInstance == null) { final T? strongInstance = _strongInstances[identifier] as T?; if (strongInstance != null) { - // This cast is safe since it matches the argument type for - // _addInstanceWithIdentifier, which is the only place _copyCallbacks - // is populated. - final T Function(T) copyCallback = - _copyCallbacks[identifier]! as T Function(T); - final T copy = copyCallback(strongInstance); + final Function copyCallback = _copyCallbacks[identifier]!; + // This avoid_dynamic_calls is safe since the type of strongInstance + // matches the argument type for _addInstanceWithIdentifier, which is + // the only place _copyCallbacks is populated. + // ignore: avoid_dynamic_calls + final T copy = copyCallback(strongInstance) as T; _identifiers[copy] = identifier; _weakInstances[identifier] = WeakReference(copy); _finalizer.attach(copy, identifier, detach: copy); diff --git a/packages/camera/camera_android_camerax/lib/src/live_data.dart b/packages/camera/camera_android_camerax/lib/src/live_data.dart new file mode 100644 index 000000000000..2a3c846d3037 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/live_data.dart @@ -0,0 +1,194 @@ +// 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/services.dart'; +import 'package:meta/meta.dart'; + +import 'camera_state.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; +import 'observer.dart'; +import 'zoom_state.dart'; + +/// A data holder class that can be observed. +/// +/// For this wrapped class, observation can only fall within the lifecycle of the +/// Android Activity to which this plugin is attached. +/// +/// See https://developer.android.com/reference/androidx/lifecycle/LiveData. +class LiveData extends JavaObject { + /// Constructs a [LiveData] that is not automatically attached to a native object. + LiveData.detached({this.binaryMessenger, this.instanceManager}) + : _api = _LiveDataHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager), + super.detached( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + + final _LiveDataHostApiImpl _api; + + /// Receives binary data across the Flutter platform barrier. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager? instanceManager; + + /// Error message indicating a [LiveData] instance was constructed with a type + /// currently unsupported by the wrapping of this class. + static const String unsupportedLiveDataTypeErrorMessage = + 'The type of LiveData passed to this method is current unspported; please see LiveDataSupportedTypeData in pigeons/camerax_library.dart if you wish to support a new type.'; + + /// Adds specified [Observer] to the list of observers of this instance. + Future observe(Observer observer) { + return _api.observeFromInstances(this, observer); + } + + /// Removes all observers of this instance. + Future removeObservers() { + return _api.removeObserversFromInstances(this); + } + + /// Returns the current value. + Future? getValue() { + return _api.getValueFromInstances(this); + } +} + +/// Host API implementation of [LiveData]. +class _LiveDataHostApiImpl extends LiveDataHostApi { + /// Constructs a [_LiveDataHostApiImpl]. + /// + /// If [binaryMessenger] the default BinaryMessenger will be used which routes to + /// the host platform. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. + _LiveDataHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Receives binary data across the Flutter platform barrier. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + /// Adds specified [Observer] to the list of observers of the specified + /// [LiveData] instance. + Future observeFromInstances( + LiveData instance, + Observer observer, + ) { + return observe( + instanceManager.getIdentifier(instance)!, + instanceManager.getIdentifier(observer)!, + ); + } + + /// Removes all observers of the specified [LiveData] instance. + Future removeObserversFromInstances( + LiveData instance, + ) { + return removeObservers( + instanceManager.getIdentifier(instance)!, + ); + } + + /// Gets current value of specified [LiveData] instance. + Future? getValueFromInstances( + LiveData instance) async { + LiveDataSupportedTypeData? typeData; + switch (T) { + case CameraState: + typeData = + LiveDataSupportedTypeData(value: LiveDataSupportedType.cameraState); + break; + case ZoomState: + typeData = + LiveDataSupportedTypeData(value: LiveDataSupportedType.zoomState); + break; + default: + throw ArgumentError(LiveData.unsupportedLiveDataTypeErrorMessage); + } + final int? valueIdentifier = + await getValue(instanceManager.getIdentifier(instance)!, typeData); + return valueIdentifier == null + ? null + : instanceManager.getInstanceWithWeakReference(valueIdentifier); + } +} + +/// Flutter API implementation for [LiveData]. +/// +/// 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 LiveDataFlutterApiImpl implements LiveDataFlutterApi { + /// Constructs a [LiveDataFlutterApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. + LiveDataFlutterApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _binaryMessenger = binaryMessenger, + _instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + final BinaryMessenger? _binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager _instanceManager; + + @override + void create( + int identifier, + LiveDataSupportedTypeData typeData, + ) { + switch (typeData.value) { + case LiveDataSupportedType.cameraState: + _instanceManager.addHostCreatedInstance( + LiveData.detached( + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, + ), + identifier, + onCopy: (LiveData original) => + LiveData.detached( + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, + ), + ); + return; + case LiveDataSupportedType.zoomState: + _instanceManager.addHostCreatedInstance( + LiveData.detached( + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, + ), + identifier, + onCopy: (LiveData original) => + LiveData.detached( + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, + ), + ); + return; + // This ignore statement is safe beause this error will be useful when + // a new LiveDataSupportedType is being added, but the logic in this method + // has not yet been updated. + // ignore: no_default_cases + default: + throw ArgumentError(LiveData.unsupportedLiveDataTypeErrorMessage); + } + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/observer.dart b/packages/camera/camera_android_camerax/lib/src/observer.dart new file mode 100644 index 000000000000..f3b9a8fd5003 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/observer.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 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; +import 'live_data.dart'; + +/// Callback that can receive from [LiveData]. +/// +/// See https://developer.android.com/reference/androidx/lifecycle/Observer. +class Observer extends JavaObject { + /// Constructor for [Observer]. + Observer( + {super.binaryMessenger, + super.instanceManager, + required void Function(Object value) onChanged}) + : _api = _ObserverHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager), + super.detached() { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + this.onChanged = (Object value) { + if (value is! T) { + throw ArgumentError( + 'The type of value observed does not match the type of Observer constructed.'); + } + onChanged(value); + }; + _api.createFromInstance(this); + } + + /// Constructs a [Observer] that is not automatically attached to a native object. + Observer.detached( + {super.binaryMessenger, + super.instanceManager, + required void Function(Object value) onChanged}) + : _api = _ObserverHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager), + super.detached() { + this.onChanged = (Object value) { + assert(value is T); + onChanged(value); + }; + } + + final _ObserverHostApiImpl _api; + + /// Callback used when the observed data is changed to a new value. + /// + /// The callback parameter cannot take type [T] directly due to the issue + /// described in https://github.com/dart-lang/sdk/issues/51461. + late final void Function(Object value) onChanged; +} + +class _ObserverHostApiImpl extends ObserverHostApi { + /// Constructs an [_ObserverHostApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. + _ObserverHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + final BinaryMessenger? binaryMessenger; + + final InstanceManager instanceManager; + + /// Adds specified [Observer] instance to instance manager and makes call + /// to native side to create the instance. + Future createFromInstance( + Observer instance, + ) { + return create( + instanceManager.addDartCreatedInstance( + instance, + onCopy: (Observer original) => Observer.detached( + onChanged: original.onChanged, + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + ), + ); + } +} + +/// Flutter API implementation for [Observer]. +/// +/// 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 ObserverFlutterApiImpl implements ObserverFlutterApi { + /// Constructs an [ObserverFlutterApiImpl]. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. + ObserverFlutterApiImpl({ + InstanceManager? instanceManager, + }) : _instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager _instanceManager; + + @override + void onChanged( + int identifier, + int valueIdentifier, + ) { + final Observer instance = + _instanceManager.getInstanceWithWeakReference(identifier)!; + + // This call is safe because the onChanged callback will check the type + // of the instance to ensure it is expected before proceeding. + // ignore: avoid_dynamic_calls, void_checks + instance.onChanged( + _instanceManager.getInstanceWithWeakReference(valueIdentifier)!, + ); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/pending_recording.dart b/packages/camera/camera_android_camerax/lib/src/pending_recording.dart new file mode 100644 index 000000000000..179eaf855294 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/pending_recording.dart @@ -0,0 +1,101 @@ +// 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/services.dart' show BinaryMessenger; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; +import 'recording.dart'; + +/// Dart wrapping of PendingRecording CameraX class. +/// +/// See https://developer.android.com/reference/androidx/camera/video/PendingRecording +class PendingRecording extends JavaObject { + /// Creates a [PendingRecording] that is not automatically attached to + /// a native object. + PendingRecording.detached( + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = PendingRecordingHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + late final PendingRecordingHostApiImpl _api; + + /// Starts the recording, making it an active recording. + Future start() { + return _api.startFromInstance(this); + } +} + +/// Host API implementation of [PendingRecording]. +class PendingRecordingHostApiImpl extends PendingRecordingHostApi { + /// Constructs a PendingRecordingHostApiImpl. + PendingRecordingHostApiImpl( + {this.binaryMessenger, InstanceManager? instanceManager}) + : super(binaryMessenger: binaryMessenger) { + this.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. + late final InstanceManager instanceManager; + + /// Starts the recording, making it an active recording. + Future startFromInstance(PendingRecording pendingRecording) async { + int? instanceId = instanceManager.getIdentifier(pendingRecording); + instanceId ??= instanceManager.addDartCreatedInstance(pendingRecording, + onCopy: (PendingRecording original) { + return PendingRecording.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + }); + return instanceManager + .getInstanceWithWeakReference(await start(instanceId))! as Recording; + } +} + +/// Flutter API implementation of [PendingRecording]. +class PendingRecordingFlutterApiImpl extends PendingRecordingFlutterApi { + /// Constructs a [PendingRecordingFlutterApiImpl]. + PendingRecordingFlutterApiImpl({ + 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( + PendingRecording.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + identifier, onCopy: (PendingRecording original) { + return PendingRecording.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + }); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/plane_proxy.dart b/packages/camera/camera_android_camerax/lib/src/plane_proxy.dart index c057876d1ede..d5b1661b0b40 100644 --- a/packages/camera/camera_android_camerax/lib/src/plane_proxy.dart +++ b/packages/camera/camera_android_camerax/lib/src/plane_proxy.dart @@ -46,23 +46,25 @@ class PlaneProxy extends JavaObject { /// overridden native class. @protected class PlaneProxyFlutterApiImpl implements PlaneProxyFlutterApi { - /// Constructs a [PlaneProxyFlutterApiImpl]. + /// Constructs an [PlaneProxyFlutterApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. /// /// An [instanceManager] is typically passed when a copy of an instance - /// contained by an `InstanceManager` is being created. + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. PlaneProxyFlutterApiImpl({ - this.binaryMessenger, + BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + }) : _binaryMessenger = binaryMessenger, + _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; + final BinaryMessenger? _binaryMessenger; /// Maintains instances stored to communicate with native language objects. - final InstanceManager instanceManager; + final InstanceManager _instanceManager; @override void create( @@ -71,18 +73,18 @@ class PlaneProxyFlutterApiImpl implements PlaneProxyFlutterApi { int pixelStride, int rowStride, ) { - instanceManager.addHostCreatedInstance( + _instanceManager.addHostCreatedInstance( PlaneProxy.detached( - binaryMessenger: binaryMessenger, - instanceManager: instanceManager, + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager, buffer: buffer, pixelStride: pixelStride, rowStride: rowStride, ), identifier, onCopy: (PlaneProxy original) => PlaneProxy.detached( - binaryMessenger: binaryMessenger, - instanceManager: instanceManager, + 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 02a540a3b507..c3094d714179 100644 --- a/packages/camera/camera_android_camerax/lib/src/preview.dart +++ b/packages/camera/camera_android_camerax/lib/src/preview.dart @@ -70,18 +70,19 @@ class Preview extends UseCase { /// Host API implementation of [Preview]. class PreviewHostApiImpl extends PreviewHostApi { - /// Constructs a [PreviewHostApiImpl]. + /// Constructs an [PreviewHostApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. /// /// An [instanceManager] is typically passed when a copy of an instance - /// contained by an `InstanceManager` is being created. + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. PreviewHostApiImpl({this.binaryMessenger, InstanceManager? instanceManager}) { this.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. @@ -106,10 +107,8 @@ class PreviewHostApiImpl extends PreviewHostApi { /// the ID corresponding to the surface it will provide. Future setSurfaceProviderFromInstance(Preview instance) async { final int? identifier = instanceManager.getIdentifier(instance); - assert(identifier != null, - 'No Preview has the identifer of that requested to set the surface provider on.'); - final int surfaceTextureEntryId = await setSurfaceProvider(identifier!); + return surfaceTextureEntryId; } @@ -122,10 +121,8 @@ class PreviewHostApiImpl extends PreviewHostApi { /// Gets the resolution information of the specified [Preview] instance. Future getResolutionInfoFromInstance(Preview instance) async { final int? identifier = instanceManager.getIdentifier(instance); - assert(identifier != null, - 'No Preview has the identifer of that requested to get the resolution information for.'); - final ResolutionInfo resolutionInfo = await getResolutionInfo(identifier!); + return resolutionInfo; } } 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 d8cccdb2aa3e..88eab07f0bed 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 @@ -74,10 +74,14 @@ class ProcessCameraProvider extends JavaObject { /// Host API implementation of [ProcessCameraProvider]. class ProcessCameraProviderHostApiImpl extends ProcessCameraProviderHostApi { - /// Creates a [ProcessCameraProviderHostApiImpl]. + /// Constructs an [ProcessCameraProviderHostApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. /// /// An [instanceManager] is typically passed when a copy of an instance - /// contained by an `InstanceManager` is being created. + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. ProcessCameraProviderHostApiImpl( {this.binaryMessenger, InstanceManager? instanceManager}) : super(binaryMessenger: binaryMessenger) { @@ -85,9 +89,6 @@ class ProcessCameraProviderHostApiImpl extends ProcessCameraProviderHostApi { } /// 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. @@ -104,9 +105,6 @@ class ProcessCameraProviderHostApiImpl extends ProcessCameraProviderHostApi { /// the [ProcessCameraProvider] instance. int getProcessCameraProviderIdentifier(ProcessCameraProvider instance) { final int? identifier = instanceManager.getIdentifier(instance); - - assert(identifier != null, - 'No ProcessCameraProvider has the identifer of that which was requested.'); return identifier!; } @@ -186,30 +184,36 @@ class ProcessCameraProviderHostApiImpl extends ProcessCameraProviderHostApi { /// Flutter API Implementation of [ProcessCameraProvider]. class ProcessCameraProviderFlutterApiImpl implements ProcessCameraProviderFlutterApi { - /// Constructs a [ProcessCameraProviderFlutterApiImpl]. + /// Constructs an [ProcessCameraProviderFlutterApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. ProcessCameraProviderFlutterApiImpl({ - this.binaryMessenger, + BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + }) : _binaryMessenger = binaryMessenger, + _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; + final BinaryMessenger? _binaryMessenger; /// Maintains instances stored to communicate with native language objects. - final InstanceManager instanceManager; + final InstanceManager _instanceManager; @override void create(int identifier) { - instanceManager.addHostCreatedInstance( + _instanceManager.addHostCreatedInstance( ProcessCameraProvider.detached( - binaryMessenger: binaryMessenger, instanceManager: instanceManager), + binaryMessenger: _binaryMessenger, instanceManager: _instanceManager), identifier, onCopy: (ProcessCameraProvider original) { return ProcessCameraProvider.detached( - binaryMessenger: binaryMessenger, instanceManager: instanceManager); + binaryMessenger: _binaryMessenger, + instanceManager: _instanceManager); }, ); } diff --git a/packages/camera/camera_android_camerax/lib/src/recorder.dart b/packages/camera/camera_android_camerax/lib/src/recorder.dart new file mode 100644 index 000000000000..016d7f776f8f --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/recorder.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:flutter/services.dart'; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; +import 'pending_recording.dart'; + +/// A dart wrapping of the CameraX Recorder class. +/// +/// See https://developer.android.com/reference/androidx/camera/video/Recorder. +class Recorder extends JavaObject { + /// Creates a [Recorder]. + Recorder( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + this.aspectRatio, + this.bitRate}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + _api = RecorderHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + _api.createFromInstance(this, aspectRatio, bitRate); + } + + /// Creates a [Recorder] that is not automatically attached to a native object + Recorder.detached( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + this.aspectRatio, + this.bitRate}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = RecorderHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + late final RecorderHostApiImpl _api; + + /// The video aspect ratio of this Recorder. + final int? aspectRatio; + + /// The intended video encoding bitrate for recording. + final int? bitRate; + + /// Prepare a recording that will be saved to a file. + Future prepareRecording(String path) { + return _api.prepareRecordingFromInstance(this, path); + } +} + +/// Host API implementation of [Recorder]. +class RecorderHostApiImpl extends RecorderHostApi { + /// Constructs a [RecorderHostApiImpl]. + RecorderHostApiImpl( + {this.binaryMessenger, InstanceManager? instanceManager}) { + this.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. + late final InstanceManager instanceManager; + + /// Creates a [Recorder] with the provided aspect ratio and bitrate if specified. + void createFromInstance(Recorder instance, int? aspectRatio, int? bitRate) { + int? identifier = instanceManager.getIdentifier(instance); + identifier ??= instanceManager.addDartCreatedInstance(instance, + onCopy: (Recorder original) { + return Recorder.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + aspectRatio: aspectRatio, + bitRate: bitRate); + }); + create(identifier, aspectRatio, bitRate); + } + + /// Prepares a [Recording] using this recorder. The output file will be saved + /// at the provided path. + Future prepareRecordingFromInstance( + Recorder instance, String path) async { + final int pendingRecordingId = + await prepareRecording(instanceManager.getIdentifier(instance)!, path); + + return instanceManager.getInstanceWithWeakReference(pendingRecordingId)!; + } +} + +/// Flutter API implementation of [Recorder]. +class RecorderFlutterApiImpl extends RecorderFlutterApi { + /// Constructs a [RecorderFlutterApiImpl]. + RecorderFlutterApiImpl({ + 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? aspectRatio, int? bitRate) { + instanceManager.addHostCreatedInstance( + Recorder.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + aspectRatio: aspectRatio, + bitRate: bitRate, + ), + identifier, onCopy: (Recorder original) { + return Recorder.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + aspectRatio: aspectRatio, + bitRate: bitRate, + ); + }); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/recording.dart b/packages/camera/camera_android_camerax/lib/src/recording.dart new file mode 100644 index 000000000000..2f21e255b621 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/recording.dart @@ -0,0 +1,120 @@ +// 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/services.dart' show BinaryMessenger; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// Wraps a CameraX recording class. +/// +/// See https://developer.android.com/reference/androidx/camera/video/Recording. +class Recording extends JavaObject { + /// Constructs a detached [Recording] + Recording.detached( + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ) { + _api = RecordingHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + late final RecordingHostApiImpl _api; + + /// Closes this recording. + Future close() { + return _api.closeFromInstance(this); + } + + /// Pauses this recording if active. + Future pause() { + return _api.pauseFromInstance(this); + } + + /// Resumes the current recording if paused. + Future resume() { + return _api.resumeFromInstance(this); + } + + /// Stops the recording, as if calling close(). + Future stop() { + return _api.stopFromInstance(this); + } +} + +/// Host API implementation of [Recording]. +class RecordingHostApiImpl extends RecordingHostApi { + /// Creates a [RecordingHostApiImpl]. + RecordingHostApiImpl({this.binaryMessenger, InstanceManager? instanceManager}) + : super(binaryMessenger: binaryMessenger) { + this.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. + late final InstanceManager instanceManager; + + /// Closes the specified recording instance. + Future closeFromInstance(Recording recording) async { + close(instanceManager.getIdentifier(recording)!); + } + + /// Pauses the specified recording instance if active. + Future pauseFromInstance(Recording recording) async { + pause(instanceManager.getIdentifier(recording)!); + } + + /// Resumes the specified recording instance if paused. + Future resumeFromInstance(Recording recording) async { + resume(instanceManager.getIdentifier(recording)!); + } + + /// Stops the specified recording instance, as if calling closeFromInstance(). + Future stopFromInstance(Recording recording) async { + stop(instanceManager.getIdentifier(recording)!); + } +} + +/// Flutter API implementation of [Recording]. +class RecordingFlutterApiImpl extends RecordingFlutterApi { + /// Constructs a [RecordingFlutterApiImpl]. + RecordingFlutterApiImpl({ + 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( + Recording.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + identifier, onCopy: (Recording original) { + return Recording.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + }); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/surface.dart b/packages/camera/camera_android_camerax/lib/src/surface.dart index ea8cf8cb751e..42cad38721c0 100644 --- a/packages/camera/camera_android_camerax/lib/src/surface.dart +++ b/packages/camera/camera_android_camerax/lib/src/surface.dart @@ -8,7 +8,7 @@ import 'java_object.dart'; /// /// See https://developer.android.com/reference/android/view/Surface.html. class Surface extends JavaObject { - /// Creates a detached [UseCase]. + /// Creates a detached [Surface]. Surface.detached({super.binaryMessenger, super.instanceManager}) : super.detached(); diff --git a/packages/camera/camera_android_camerax/lib/src/system_services.dart b/packages/camera/camera_android_camerax/lib/src/system_services.dart index e108b6140bed..a2513e037662 100644 --- a/packages/camera/camera_android_camerax/lib/src/system_services.dart +++ b/packages/camera/camera_android_camerax/lib/src/system_services.dart @@ -61,18 +61,37 @@ class SystemServices { api.stopListeningForDeviceOrientationChange(); } + + /// Returns a file path which was used to create a temporary file. + /// Prefix is a part of the file name, and suffix is the file extension. + /// + /// The file and path constraints are determined by the implementation of + /// File.createTempFile(prefix, suffix, cacheDir), on the android side, where + /// where cacheDir is the cache directory identified by the current application + /// context using context.getCacheDir(). + /// + /// Ex: getTempFilePath('prefix', 'suffix') would return a string of the form + /// '/prefix3213453.suffix', where the numbers after prefix and + /// before suffix are determined by the call to File.createTempFile and + /// therefore random. + static Future getTempFilePath(String prefix, String suffix, + {BinaryMessenger? binaryMessenger}) { + final SystemServicesHostApi api = + SystemServicesHostApi(binaryMessenger: binaryMessenger); + return api.getTempFilePath(prefix, suffix); + } } /// Host API implementation of [SystemServices]. class SystemServicesHostApiImpl extends SystemServicesHostApi { - /// Creates a [SystemServicesHostApiImpl]. + /// Constructs an [SystemServicesHostApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. SystemServicesHostApiImpl({this.binaryMessenger}) : super(binaryMessenger: binaryMessenger); /// 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; /// Requests permission to access the camera and audio if specified. @@ -94,16 +113,8 @@ class SystemServicesHostApiImpl extends SystemServicesHostApi { /// Flutter API implementation of [SystemServices]. class SystemServicesFlutterApiImpl implements SystemServicesFlutterApi { - /// Constructs a [SystemServicesFlutterApiImpl]. - SystemServicesFlutterApiImpl({ - this.binaryMessenger, - }); - - /// 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; + /// Constructs an [SystemServicesFlutterApiImpl]. + SystemServicesFlutterApiImpl(); /// Callback method for any changes in device orientation. /// @@ -114,9 +125,6 @@ class SystemServicesFlutterApiImpl implements SystemServicesFlutterApi { void onDeviceOrientationChanged(String orientation) { final DeviceOrientation deviceOrientation = deserializeDeviceOrientation(orientation); - if (deviceOrientation == null) { - return; - } SystemServices.deviceOrientationChangedStreamController .add(DeviceOrientationChangedEvent(deviceOrientation)); } diff --git a/packages/camera/camera_android_camerax/lib/src/use_case.dart b/packages/camera/camera_android_camerax/lib/src/use_case.dart index f8910d9c5347..256caf8c01bc 100644 --- a/packages/camera/camera_android_camerax/lib/src/use_case.dart +++ b/packages/camera/camera_android_camerax/lib/src/use_case.dart @@ -4,7 +4,7 @@ import 'java_object.dart'; -/// An object representing the different functionalitites of the camera. +/// An object representing the different functionalities of the camera. /// /// See https://developer.android.com/reference/androidx/camera/core/UseCase. class UseCase extends JavaObject { diff --git a/packages/camera/camera_android_camerax/lib/src/video_capture.dart b/packages/camera/camera_android_camerax/lib/src/video_capture.dart new file mode 100644 index 000000000000..0c624d159ed6 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/video_capture.dart @@ -0,0 +1,116 @@ +// 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/services.dart'; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; +import 'recorder.dart'; +import 'use_case.dart'; + +/// Dart wrapping of CameraX VideoCapture class. +/// +/// See https://developer.android.com/reference/androidx/camera/video/VideoCapture. +class VideoCapture extends UseCase { + /// Creates a VideoCapture that is not automatically attached to a native object. + VideoCapture.detached( + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = VideoCaptureHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + /// Creates a [VideoCapture] associated with the given [Recorder]. + static Future withOutput(Recorder recorder, + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + final VideoCaptureHostApiImpl api = VideoCaptureHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + + return api.withOutputFromInstance(recorder); + } + + /// Gets the [Recorder] associated with this VideoCapture. + Future getOutput() { + return _api.getOutputFromInstance(this); + } + + late final VideoCaptureHostApiImpl _api; +} + +/// Host API implementation of [VideoCapture]. +class VideoCaptureHostApiImpl extends VideoCaptureHostApi { + /// Constructs a [VideoCaptureHostApiImpl]. + VideoCaptureHostApiImpl( + {this.binaryMessenger, InstanceManager? instanceManager}) { + this.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. + late final InstanceManager instanceManager; + + /// Creates a [VideoCapture] associated with the provided [Recorder] instance. + Future withOutputFromInstance(Recorder recorder) async { + int? identifier = instanceManager.getIdentifier(recorder); + identifier ??= instanceManager.addDartCreatedInstance(recorder, + onCopy: (Recorder original) { + return Recorder( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + }); + final int videoCaptureId = await withOutput(identifier); + return instanceManager + .getInstanceWithWeakReference(videoCaptureId)!; + } + + /// Gets the [Recorder] associated with the provided [VideoCapture] instance. + Future getOutputFromInstance(VideoCapture instance) async { + final int? identifier = instanceManager.getIdentifier(instance); + final int recorderId = await getOutput(identifier!); + return instanceManager.getInstanceWithWeakReference(recorderId)!; + } +} + +/// Flutter API implementation of [VideoCapture]. +class VideoCaptureFlutterApiImpl implements VideoCaptureFlutterApi { + /// Constructs a [VideoCaptureFlutterApiImpl]. + VideoCaptureFlutterApiImpl({ + 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( + VideoCapture.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + identifier, onCopy: (VideoCapture original) { + return VideoCapture.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + }); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/zoom_state.dart b/packages/camera/camera_android_camerax/lib/src/zoom_state.dart new file mode 100644 index 000000000000..98068820b154 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/zoom_state.dart @@ -0,0 +1,71 @@ +// 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/services.dart' show BinaryMessenger; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// Represents zoom related information of a camera. +/// +/// See https://developer.android.com/reference/androidx/camera/core/ZoomState. +class ZoomState extends JavaObject { + /// Constructs a [CameraInfo] that is not automatically attached to a native object. + ZoomState.detached( + {super.binaryMessenger, + super.instanceManager, + required this.minZoomRatio, + required this.maxZoomRatio}) + : super.detached() { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + /// The minimum zoom ratio of the camera represented by this instance. + final double minZoomRatio; + + /// The maximum zoom ratio of the camera represented by this instance. + final double maxZoomRatio; +} + +/// Flutter API implementation of [ZoomState]. +class ZoomStateFlutterApiImpl implements ZoomStateFlutterApi { + /// Constructs a [ZoomStateFlutterApiImpl]. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. + ZoomStateFlutterApiImpl({ + 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, double minZoomRatio, double maxZoomRatio) { + instanceManager.addHostCreatedInstance( + ZoomState.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + minZoomRatio: minZoomRatio, + maxZoomRatio: maxZoomRatio), + identifier, + onCopy: (ZoomState original) { + return ZoomState.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + minZoomRatio: original.minZoomRatio, + maxZoomRatio: original.maxZoomRatio); + }, + ); + } +} diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index 3d492709c900..601601dbf365 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -46,6 +46,56 @@ class CameraPermissionsErrorData { String description; } +/// The states the camera can be in. +/// +/// See https://developer.android.com/reference/androidx/camera/core/CameraState.Type. +enum CameraStateType { + closed, + closing, + open, + opening, + pendingOpen, +} + +class CameraStateTypeData { + late CameraStateType value; +} + +/// The types (T) properly wrapped to be used as a LiveData. +/// +/// If you need to add another type to support a type S to use a LiveData in +/// this plugin, ensure the following is done on the Dart side: +/// +/// * In `../lib/src/live_data.dart`, add new cases for S in +/// `_LiveDataHostApiImpl#getValueFromInstances` to get the current value of +/// type S from a LiveData instance and in `LiveDataFlutterApiImpl#create` +/// to create the expected type of LiveData when requested. +/// +/// On the native side, ensure the following is done: +/// +/// * Update `LiveDataHostApiImpl#getValue` is updated to properly return +/// identifiers for instances of type S. +/// * Update `ObserverFlutterApiWrapper#onChanged` to properly handle receiving +/// calls with instances of type S if a LiveData instance is observed. +enum LiveDataSupportedType { + cameraState, + zoomState, +} + +class LiveDataSupportedTypeData { + late LiveDataSupportedType value; +} + +class ExposureCompensationRange { + ExposureCompensationRange({ + required this.minCompensation, + required this.maxCompensation, + }); + + int minCompensation; + int maxCompensation; +} + @HostApi(dartHostTestHandler: 'TestInstanceManagerHostApi') abstract class InstanceManagerHostApi { /// Clear the native `InstanceManager`. @@ -67,6 +117,12 @@ abstract class JavaObjectFlutterApi { @HostApi(dartHostTestHandler: 'TestCameraInfoHostApi') abstract class CameraInfoHostApi { int getSensorRotationDegrees(int identifier); + + int getCameraState(int identifier); + + int getExposureState(int identifier); + + int getZoomState(int identifier); } @FlutterApi() @@ -108,6 +164,11 @@ abstract class ProcessCameraProviderFlutterApi { void create(int identifier); } +@HostApi(dartHostTestHandler: 'TestCameraHostApi') +abstract class CameraHostApi { + int getCameraInfo(int identifier); +} + @FlutterApi() abstract class CameraFlutterApi { void create(int identifier); @@ -122,6 +183,8 @@ abstract class SystemServicesHostApi { bool isFrontFacing, int sensorOrientation); void stopListeningForDeviceOrientationChange(); + + String getTempFilePath(String prefix, String suffix); } @FlutterApi() @@ -142,6 +205,60 @@ abstract class PreviewHostApi { ResolutionInfo getResolutionInfo(int identifier); } +@HostApi(dartHostTestHandler: 'TestVideoCaptureHostApi') +abstract class VideoCaptureHostApi { + int withOutput(int videoOutputId); + + int getOutput(int identifier); +} + +@FlutterApi() +abstract class VideoCaptureFlutterApi { + void create(int identifier); +} + +@HostApi(dartHostTestHandler: 'TestRecorderHostApi') +abstract class RecorderHostApi { + void create(int identifier, int? aspectRatio, int? bitRate); + + int getAspectRatio(int identifier); + + int getTargetVideoEncodingBitRate(int identifier); + + int prepareRecording(int identifier, String path); +} + +@FlutterApi() +abstract class RecorderFlutterApi { + void create(int identifier, int? aspectRatio, int? bitRate); +} + +@HostApi(dartHostTestHandler: 'TestPendingRecordingHostApi') +abstract class PendingRecordingHostApi { + int start(int identifier); +} + +@FlutterApi() +abstract class PendingRecordingFlutterApi { + void create(int identifier); +} + +@HostApi(dartHostTestHandler: 'TestRecordingHostApi') +abstract class RecordingHostApi { + void close(int identifier); + + void pause(int identifier); + + void resume(int identifier); + + void stop(int identifier); +} + +@FlutterApi() +abstract class RecordingFlutterApi { + void create(int identifier); +} + @HostApi(dartHostTestHandler: 'TestImageCaptureHostApi') abstract class ImageCaptureHostApi { void create(int identifier, int? flashMode, ResolutionInfo? targetResolution); @@ -152,6 +269,24 @@ abstract class ImageCaptureHostApi { String takePicture(int identifier); } +@FlutterApi() +abstract class CameraStateFlutterApi { + void create(int identifier, CameraStateTypeData type, int? errorIdentifier); +} + +@FlutterApi() +abstract class ExposureStateFlutterApi { + void create( + int identifier, + ExposureCompensationRange exposureCompensationRange, + double exposureCompensationStep); +} + +@FlutterApi() +abstract class ZoomStateFlutterApi { + void create(int identifier, double minZoomRatio, double maxZoomRatio); +} + @HostApi(dartHostTestHandler: 'TestImageAnalysisHostApi') abstract class ImageAnalysisHostApi { void create(int identifier, ResolutionInfo? targetResolutionIdentifier); @@ -166,6 +301,35 @@ abstract class AnalyzerHostApi { void create(int identifier); } +@HostApi(dartHostTestHandler: 'TestObserverHostApi') +abstract class ObserverHostApi { + void create(int identifier); +} + +@FlutterApi() +abstract class ObserverFlutterApi { + void onChanged(int identifier, int valueIdentifier); +} + +@FlutterApi() +abstract class CameraStateErrorFlutterApi { + void create(int identifier, int code); +} + +@HostApi(dartHostTestHandler: 'TestLiveDataHostApi') +abstract class LiveDataHostApi { + void observe(int identifier, int observerIdentifier); + + void removeObservers(int identifier); + + int? getValue(int identifier, LiveDataSupportedTypeData type); +} + +@FlutterApi() +abstract class LiveDataFlutterApi { + void create(int identifier, LiveDataSupportedTypeData type); +} + @FlutterApi() abstract class AnalyzerFlutterApi { void create(int identifier); diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index f4630a7ab26f..7275c68f1ff4 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,11 +2,12 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -publish_to: 'none' + +version: 0.5.0+4 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=2.19.0 <4.0.0" + flutter: ">=3.7.0" flutter: plugin: @@ -18,6 +19,7 @@ flutter: dartPluginClass: AndroidCameraCameraX dependencies: + async: ^2.5.0 camera_platform_interface: ^2.2.0 flutter: sdk: flutter @@ -27,7 +29,6 @@ dependencies: stream_transform: ^2.1.0 dev_dependencies: - async: ^2.5.0 build_runner: ^2.2.0 flutter_test: sdk: flutter 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 2db12ebde63b..d6e2531cf084 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 @@ -10,15 +10,25 @@ 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/camera_state.dart'; +import 'package:camera_android_camerax/src/camera_state_error.dart'; import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/exposure_state.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/live_data.dart'; +import 'package:camera_android_camerax/src/observer.dart'; +import 'package:camera_android_camerax/src/pending_recording.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/recorder.dart'; +import 'package:camera_android_camerax/src/recording.dart'; import 'package:camera_android_camerax/src/system_services.dart'; import 'package:camera_android_camerax/src/use_case.dart'; +import 'package:camera_android_camerax/src/video_capture.dart'; +import 'package:camera_android_camerax/src/zoom_state.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/services.dart' show DeviceOrientation, Uint8List; import 'package:flutter/widgets.dart'; @@ -34,25 +44,67 @@ import 'test_camerax_library.g.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), MockSpec(), MockSpec(), MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), MockSpec(), + MockSpec(), + MockSpec(), +]) +@GenerateMocks([], customMocks: >[ + MockSpec>(as: #MockLiveCameraState), + MockSpec>(as: #MockLiveZoomState), ]) -@GenerateMocks([BuildContext]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); // Mocks the call to clear the native InstanceManager. TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + /// Helper method for testing sending/receiving CameraErrorEvents. + Future testCameraClosingObserver(AndroidCameraCameraX camera, + int cameraId, Observer observer) async { + final CameraStateError testCameraStateError = + CameraStateError.detached(code: 0); + final Stream cameraClosingEventStream = + camera.onCameraClosing(cameraId); + final StreamQueue cameraClosingStreamQueue = + StreamQueue(cameraClosingEventStream); + final Stream cameraErrorEventStream = + camera.onCameraError(cameraId); + final StreamQueue cameraErrorStreamQueue = + StreamQueue(cameraErrorEventStream); + + observer.onChanged(CameraState.detached( + type: CameraStateType.closing, error: testCameraStateError)); + + final bool cameraClosingEventSent = + await cameraClosingStreamQueue.next == CameraClosingEvent(cameraId); + final bool cameraErrorSent = await cameraErrorStreamQueue.next == + CameraErrorEvent(cameraId, testCameraStateError.getDescription()); + + await cameraClosingStreamQueue.cancel(); + await cameraErrorStreamQueue.cancel(); + + return cameraClosingEventSent && cameraErrorSent; + } + + // 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 FakeAndroidCameraCameraX camera = FakeAndroidCameraCameraX(); camera.processCameraProvider = MockProcessCameraProvider(); final List returnData = [ { @@ -110,10 +162,12 @@ void main() { }); test( - 'createCamera requests permissions, starts listening for device orientation changes, and returns flutter surface texture ID', + 'createCamera requests permissions, starts listening for device orientation changes, updates camera state observers, and returns flutter surface texture ID', () async { - final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); - camera.processCameraProvider = MockProcessCameraProvider(); + final FakeAndroidCameraCameraX camera = + FakeAndroidCameraCameraX(shouldCreateDetachedObjectForTesting: true); + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); const CameraLensDirection testLensDirection = CameraLensDirection.back; const int testSensorOrientation = 90; const CameraDescription testCameraDescription = CameraDescription( @@ -123,9 +177,20 @@ void main() { const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; const bool enableAudio = true; const int testSurfaceTextureId = 6; + final MockCamera mockCamera = MockCamera(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockLiveCameraState mockLiveCameraState = MockLiveCameraState(); when(camera.testPreview.setSurfaceProvider()) .thenAnswer((_) async => testSurfaceTextureId); + when(mockProcessCameraProvider.bindToLifecycle( + camera.mockBackCameraSelector, + [camera.testPreview, camera.testImageCapture])) + .thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); + when(mockCameraInfo.getCameraState()) + .thenAnswer((_) async => mockLiveCameraState); + camera.processCameraProvider = mockProcessCameraProvider; expect( await camera.createCamera(testCameraDescription, testResolutionPreset, @@ -145,15 +210,30 @@ void main() { // Verify the camera's ImageCapture instance is instantiated properly. expect(camera.imageCapture, equals(camera.testImageCapture)); + // Verify the camera's Recorder and VideoCapture instances are instantiated properly. + expect(camera.recorder, equals(camera.testRecorder)); + expect(camera.videoCapture, equals(camera.testVideoCapture)); + // Verify the camera's Preview instance has its surface provider set. verify(camera.preview!.setSurfaceProvider()); + + // Verify the camera state observer is updated. + expect( + await testCameraClosingObserver( + camera, + testSurfaceTextureId, + verify(mockLiveCameraState.observe(captureAny)).captured.single + as Observer), + isTrue); }); test( 'createCamera binds Preview and ImageCapture use cases to ProcessCameraProvider instance', () async { - final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); - camera.processCameraProvider = MockProcessCameraProvider(); + final FakeAndroidCameraCameraX camera = + FakeAndroidCameraCameraX(shouldCreateDetachedObjectForTesting: true); + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); const CameraLensDirection testLensDirection = CameraLensDirection.back; const int testSensorOrientation = 90; const CameraDescription testCameraDescription = CameraDescription( @@ -162,12 +242,29 @@ void main() { sensorOrientation: testSensorOrientation); const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; const bool enableAudio = true; + final MockCamera mockCamera = MockCamera(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + + camera.processCameraProvider = mockProcessCameraProvider; + + when(mockProcessCameraProvider.bindToLifecycle( + camera.mockBackCameraSelector, + [camera.testPreview, camera.testImageCapture])) + .thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); + when(mockCameraInfo.getCameraState()) + .thenAnswer((_) async => MockLiveCameraState()); + camera.processCameraProvider = mockProcessCameraProvider; await camera.createCamera(testCameraDescription, testResolutionPreset, enableAudio: enableAudio); + // Verify expected UseCases were bound. verify(camera.processCameraProvider!.bindToLifecycle(camera.cameraSelector!, [camera.testPreview, camera.testImageCapture])); + + // Verify the camera's CameraInfo instance got updated. + expect(camera.cameraInfo, equals(mockCameraInfo)); }); test( @@ -178,8 +275,10 @@ void main() { }); test('initializeCamera sends expected CameraInitializedEvent', () async { - final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); - camera.processCameraProvider = MockProcessCameraProvider(); + final FakeAndroidCameraCameraX camera = + FakeAndroidCameraCameraX(shouldCreateDetachedObjectForTesting: true); + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); const int cameraId = 10; const CameraLensDirection testLensDirection = CameraLensDirection.back; const int testSensorOrientation = 90; @@ -194,9 +293,10 @@ void main() { final Camera mockCamera = MockCamera(); final ResolutionInfo testResolutionInfo = ResolutionInfo(width: resolutionWidth, height: resolutionHeight); + final CameraInfo mockCameraInfo = MockCameraInfo(); // TODO(camsim99): Modify this when camera configuration is supported and - // defualt values no longer being used. + // default values no longer being used. // https://github.com/flutter/flutter/issues/120468 // https://github.com/flutter/flutter/issues/120467 final CameraInitializedEvent testCameraInitializedEvent = @@ -209,18 +309,25 @@ void main() { FocusMode.auto, false); + camera.processCameraProvider = mockProcessCameraProvider; + // Call createCamera. when(camera.testPreview.setSurfaceProvider()) .thenAnswer((_) async => cameraId); - await camera.createCamera(testCameraDescription, testResolutionPreset, - enableAudio: enableAudio); - when(camera.processCameraProvider!.bindToLifecycle(camera.cameraSelector!, + when(camera.processCameraProvider!.bindToLifecycle( + camera.mockBackCameraSelector, [camera.testPreview, camera.testImageCapture])) .thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); + when(mockCameraInfo.getCameraState()) + .thenAnswer((_) async => MockLiveCameraState()); when(camera.testPreview.getResolutionInfo()) .thenAnswer((_) async => testResolutionInfo); + await camera.createCamera(testCameraDescription, testResolutionPreset, + enableAudio: enableAudio); + // Start listening to camera events stream to verify the proper CameraInitializedEvent is sent. camera.cameraEventStreamController.stream.listen((CameraEvent event) { expect(event, const TypeMatcher()); @@ -233,17 +340,22 @@ void main() { expect(camera.camera, isNotNull); }); - test('dispose releases Flutter surface texture and unbinds all use cases', + test( + 'dispose releases Flutter surface texture, removes camera state observers, and unbinds all use cases', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); camera.preview = MockPreview(); camera.processCameraProvider = MockProcessCameraProvider(); + camera.liveCameraState = MockLiveCameraState(); + camera.imageAnalysis = MockImageAnalysis(); camera.dispose(3); verify(camera.preview!.releaseFlutterSurfaceTexture()); + verify(camera.liveCameraState!.removeObservers()); verify(camera.processCameraProvider!.unbindAll()); + verify(camera.imageAnalysis!.clearAnalyzer()); }); test('onCameraInitialized stream emits CameraInitializedEvents', () async { @@ -262,18 +374,43 @@ void main() { await streamQueue.cancel(); }); - test('onCameraError stream emits errors caught by system services', () async { + test( + 'onCameraClosing stream emits camera closing event when cameraEventStreamController emits a camera closing event', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 99; + const CameraClosingEvent cameraClosingEvent = CameraClosingEvent(cameraId); + final Stream eventStream = + camera.onCameraClosing(cameraId); + final StreamQueue streamQueue = + StreamQueue(eventStream); + + camera.cameraEventStreamController.add(cameraClosingEvent); + + expect(await streamQueue.next, equals(cameraClosingEvent)); + await streamQueue.cancel(); + }); + + test( + 'onCameraError stream emits errors caught by system services or added to stream within plugin', + () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); const int cameraId = 27; - const String testErrorDescription = 'Test error description!'; + const String firstTestErrorDescription = 'Test error description 1!'; + const String secondTestErrorDescription = 'Test error description 2!'; + const CameraErrorEvent secondCameraErrorEvent = + CameraErrorEvent(cameraId, secondTestErrorDescription); final Stream eventStream = camera.onCameraError(cameraId); final StreamQueue streamQueue = StreamQueue(eventStream); - SystemServices.cameraErrorStreamController.add(testErrorDescription); - + SystemServices.cameraErrorStreamController.add(firstTestErrorDescription); expect(await streamQueue.next, - equals(const CameraErrorEvent(cameraId, testErrorDescription))); + equals(const CameraErrorEvent(cameraId, firstTestErrorDescription))); + + camera.cameraEventStreamController.add(secondCameraErrorEvent); + expect(await streamQueue.next, equals(secondCameraErrorEvent)); + await streamQueue.cancel(); }); @@ -324,47 +461,96 @@ void main() { camera.processCameraProvider!.unbind([camera.preview!])); }); - test('resumePreview does not bind preview to lifecycle if already bound', + test( + 'resumePreview does not bind preview to lifecycle or update camera state observers if already bound', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final MockCamera mockCamera = MockCamera(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockLiveCameraState mockLiveCameraState = MockLiveCameraState(); - camera.processCameraProvider = MockProcessCameraProvider(); + camera.processCameraProvider = mockProcessCameraProvider; camera.cameraSelector = MockCameraSelector(); camera.preview = MockPreview(); when(camera.processCameraProvider!.isBound(camera.preview!)) .thenAnswer((_) async => true); + when(mockProcessCameraProvider + .bindToLifecycle(camera.cameraSelector, [camera.preview!])) + .thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); + when(mockCameraInfo.getCameraState()) + .thenAnswer((_) async => mockLiveCameraState); + await camera.resumePreview(78); verifyNever(camera.processCameraProvider! .bindToLifecycle(camera.cameraSelector!, [camera.preview!])); + verifyNever(mockLiveCameraState.observe(any)); + expect(camera.cameraInfo, isNot(mockCameraInfo)); }); - test('resumePreview binds preview to lifecycle if not already bound', + test( + 'resumePreview binds preview to lifecycle and updates camera state observers if not already bound', () async { - final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final AndroidCameraCameraX camera = AndroidCameraCameraX.forTesting( + shouldCreateDetachedObjectForTesting: true); + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final MockCamera mockCamera = MockCamera(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockLiveCameraState mockLiveCameraState = MockLiveCameraState(); - camera.processCameraProvider = MockProcessCameraProvider(); + camera.processCameraProvider = mockProcessCameraProvider; camera.cameraSelector = MockCameraSelector(); camera.preview = MockPreview(); + when(mockProcessCameraProvider + .bindToLifecycle(camera.cameraSelector, [camera.preview!])) + .thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); + when(mockCameraInfo.getCameraState()) + .thenAnswer((_) async => mockLiveCameraState); + await camera.resumePreview(78); verify(camera.processCameraProvider! .bindToLifecycle(camera.cameraSelector!, [camera.preview!])); + expect( + await testCameraClosingObserver( + camera, + 78, + verify(mockLiveCameraState.observe(captureAny)).captured.single + as Observer), + isTrue); + expect(camera.cameraInfo, equals(mockCameraInfo)); }); test( 'buildPreview returns a FutureBuilder that does not return a Texture until the preview is bound to the lifecycle', () async { - final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final AndroidCameraCameraX camera = AndroidCameraCameraX.forTesting( + shouldCreateDetachedObjectForTesting: true); + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final MockCamera mockCamera = MockCamera(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); const int textureId = 75; - camera.processCameraProvider = MockProcessCameraProvider(); + camera.processCameraProvider = mockProcessCameraProvider; camera.cameraSelector = MockCameraSelector(); camera.preview = MockPreview(); + when(mockProcessCameraProvider + .bindToLifecycle(camera.cameraSelector, [camera.preview!])) + .thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); + when(mockCameraInfo.getCameraState()) + .thenAnswer((_) async => MockLiveCameraState()); + final FutureBuilder previewWidget = camera.buildPreview(textureId) as FutureBuilder; @@ -385,13 +571,25 @@ void main() { test( 'buildPreview returns a FutureBuilder that returns a Texture once the preview is bound to the lifecycle', () async { - final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final AndroidCameraCameraX camera = AndroidCameraCameraX.forTesting( + shouldCreateDetachedObjectForTesting: true); + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final MockCamera mockCamera = MockCamera(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); const int textureId = 75; - camera.processCameraProvider = MockProcessCameraProvider(); + camera.processCameraProvider = mockProcessCameraProvider; camera.cameraSelector = MockCameraSelector(); camera.preview = MockPreview(); + when(mockProcessCameraProvider + .bindToLifecycle(camera.cameraSelector, [camera.preview!])) + .thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); + when(mockCameraInfo.getCameraState()) + .thenAnswer((_) async => MockLiveCameraState()); + final FutureBuilder previewWidget = camera.buildPreview(textureId) as FutureBuilder; @@ -401,6 +599,183 @@ void main() { expect(previewTexture.textureId, equals(textureId)); }); + group('video recording', () { + test( + 'startVideoRecording binds video capture use case and starts the recording', + () async { + //Set up mocks and constants. + final FakeAndroidCameraCameraX camera = FakeAndroidCameraCameraX(); + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.recorder = camera.testRecorder; + camera.videoCapture = camera.testVideoCapture; + camera.camera = MockCamera(); + final MockPendingRecording mockPendingRecording = MockPendingRecording(); + final MockRecording mockRecording = MockRecording(); + final TestSystemServicesHostApi mockSystemServicesApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockSystemServicesApi); + + const int cameraId = 17; + const String outputPath = '/temp/MOV123.temp'; + + // Mock method calls. + when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp')) + .thenReturn(outputPath); + when(camera.testRecorder.prepareRecording(outputPath)) + .thenAnswer((_) async => mockPendingRecording); + when(mockPendingRecording.start()).thenAnswer((_) async => mockRecording); + when(camera.processCameraProvider!.isBound(camera.videoCapture!)) + .thenAnswer((_) async => false); + when(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.videoCapture!])) + .thenAnswer((_) async => camera.camera!); + + await camera.startVideoRecording(cameraId); + + verify(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.videoCapture!])); + expect(camera.pendingRecording, equals(mockPendingRecording)); + expect(camera.recording, mockRecording); + }); + + test( + 'startVideoRecording binds video capture use case and starts the recording' + ' on first call, and does nothing on second call', () async { + //Set up mocks and constants. + final FakeAndroidCameraCameraX camera = FakeAndroidCameraCameraX(); + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.recorder = camera.testRecorder; + camera.videoCapture = camera.testVideoCapture; + camera.camera = MockCamera(); + final MockPendingRecording mockPendingRecording = MockPendingRecording(); + final MockRecording mockRecording = MockRecording(); + final TestSystemServicesHostApi mockSystemServicesApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockSystemServicesApi); + + const int cameraId = 17; + const String outputPath = '/temp/MOV123.temp'; + + // Mock method calls. + when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp')) + .thenReturn(outputPath); + when(camera.testRecorder.prepareRecording(outputPath)) + .thenAnswer((_) async => mockPendingRecording); + when(mockPendingRecording.start()).thenAnswer((_) async => mockRecording); + when(camera.processCameraProvider!.isBound(camera.videoCapture!)) + .thenAnswer((_) async => false); + when(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.videoCapture!])) + .thenAnswer((_) async => camera.camera!); + + await camera.startVideoRecording(cameraId); + + verify(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.videoCapture!])); + expect(camera.pendingRecording, equals(mockPendingRecording)); + expect(camera.recording, mockRecording); + + await camera.startVideoRecording(cameraId); + // Verify that each of these calls happened only once. + verify(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp')) + .called(1); + verifyNoMoreInteractions(mockSystemServicesApi); + verify(camera.testRecorder.prepareRecording(outputPath)).called(1); + verifyNoMoreInteractions(camera.testRecorder); + verify(mockPendingRecording.start()).called(1); + verifyNoMoreInteractions(mockPendingRecording); + }); + + test('pauseVideoRecording pauses the recording', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockRecording recording = MockRecording(); + camera.recording = recording; + camera.pauseVideoRecording(0); + verify(recording.pause()); + verifyNoMoreInteractions(recording); + }); + + test('resumeVideoRecording resumes the recording', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockRecording recording = MockRecording(); + camera.recording = recording; + camera.resumeVideoRecording(0); + verify(recording.resume()); + verifyNoMoreInteractions(recording); + }); + + test('stopVideoRecording stops the recording', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockRecording recording = MockRecording(); + final MockProcessCameraProvider processCameraProvider = + MockProcessCameraProvider(); + final MockVideoCapture videoCapture = MockVideoCapture(); + const String videoOutputPath = '/test/output/path'; + + camera.processCameraProvider = processCameraProvider; + camera.recording = recording; + camera.videoCapture = videoCapture; + camera.videoOutputPath = videoOutputPath; + + final XFile file = await camera.stopVideoRecording(0); + expect(file.path, videoOutputPath); + + verify(recording.close()); + verifyNoMoreInteractions(recording); + }); + + test( + 'stopVideoRecording throws a camera exception if ' + 'no recording is in progress', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const String videoOutputPath = '/test/output/path'; + + camera.recording = null; + camera.videoOutputPath = videoOutputPath; + + expect( + () => camera.stopVideoRecording(0), throwsA(isA())); + }); + + test( + 'stopVideoRecording throws a camera exception if ' + 'videoOutputPath is null, and sets recording to null', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockRecording recording = MockRecording(); + + camera.recording = recording; + camera.videoOutputPath = null; + + expect( + () => camera.stopVideoRecording(0), throwsA(isA())); + expect(camera.recording, null); + }); + + test( + 'calling stopVideoRecording twice stops the recording ' + 'and then throws a CameraException', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockRecording recording = MockRecording(); + final MockProcessCameraProvider processCameraProvider = + MockProcessCameraProvider(); + final MockVideoCapture videoCapture = MockVideoCapture(); + const String videoOutputPath = '/test/output/path'; + + camera.processCameraProvider = processCameraProvider; + camera.recording = recording; + camera.videoCapture = videoCapture; + camera.videoOutputPath = videoOutputPath; + + final XFile file = await camera.stopVideoRecording(0); + expect(file.path, videoOutputPath); + + expect( + () => camera.stopVideoRecording(0), throwsA(isA())); + }); + }); + test( 'takePicture binds and unbinds ImageCapture to lifecycle and makes call to take a picture', () async { @@ -419,14 +794,108 @@ void main() { expect(imageFile.path, equals(testPicturePath)); }); + test('getMinExposureOffset returns expected exposure offset', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final ExposureState exposureState = ExposureState.detached( + exposureCompensationRange: + ExposureCompensationRange(minCompensation: 3, maxCompensation: 4), + exposureCompensationStep: 0.2); + + camera.cameraInfo = mockCameraInfo; + + when(mockCameraInfo.getExposureState()) + .thenAnswer((_) async => exposureState); + + // We expect the minimum exposure to be the minimum exposure compensation * exposure compensation step. + // Delta is included due to avoid catching rounding errors. + expect(await camera.getMinExposureOffset(35), closeTo(0.6, 0.0000000001)); + }); + + test('getMaxExposureOffset returns expected exposure offset', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final ExposureState exposureState = ExposureState.detached( + exposureCompensationRange: + ExposureCompensationRange(minCompensation: 3, maxCompensation: 4), + exposureCompensationStep: 0.2); + + camera.cameraInfo = mockCameraInfo; + + when(mockCameraInfo.getExposureState()) + .thenAnswer((_) async => exposureState); + + // We expect the maximum exposure to be the maximum exposure compensation * exposure compensation step. + expect(await camera.getMaxExposureOffset(35), 0.8); + }); + + test('getExposureOffsetStepSize returns expected exposure offset', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final ExposureState exposureState = ExposureState.detached( + exposureCompensationRange: + ExposureCompensationRange(minCompensation: 3, maxCompensation: 4), + exposureCompensationStep: 0.2); + + camera.cameraInfo = mockCameraInfo; + + when(mockCameraInfo.getExposureState()) + .thenAnswer((_) async => exposureState); + + expect(await camera.getExposureOffsetStepSize(55), 0.2); + }); + + test('getMaxZoomLevel returns expected exposure offset', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + const double maxZoomRatio = 1; + final LiveData mockLiveZoomState = MockLiveZoomState(); + final ZoomState zoomState = + ZoomState.detached(maxZoomRatio: maxZoomRatio, minZoomRatio: 0); + + camera.cameraInfo = mockCameraInfo; + + when(mockCameraInfo.getZoomState()) + .thenAnswer((_) async => mockLiveZoomState); + when(mockLiveZoomState.getValue()).thenAnswer((_) async => zoomState); + + expect(await camera.getMaxZoomLevel(55), maxZoomRatio); + }); + + test('getMinZoomLevel returns expected exposure offset', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + const double minZoomRatio = 0; + final LiveData mockLiveZoomState = MockLiveZoomState(); + final ZoomState zoomState = + ZoomState.detached(maxZoomRatio: 1, minZoomRatio: minZoomRatio); + + camera.cameraInfo = mockCameraInfo; + + when(mockCameraInfo.getZoomState()) + .thenAnswer((_) async => mockLiveZoomState); + when(mockLiveZoomState.getValue()).thenAnswer((_) async => zoomState); + + expect(await camera.getMinZoomLevel(55), minZoomRatio); + }); + test( 'onStreamedFrameAvailable emits CameraImageData when picked up from CameraImageData stream controller', () async { - final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); + final FakeAndroidCameraCameraX camera = + FakeAndroidCameraCameraX(shouldCreateDetachedObjectForTesting: true); + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final MockCamera mockCamera = MockCamera(); const int cameraId = 22; - camera.processCameraProvider = MockProcessCameraProvider(); + + camera.processCameraProvider = mockProcessCameraProvider; camera.cameraSelector = MockCameraSelector(); - camera.createDetachedCallbacks = true; + + when(mockProcessCameraProvider.bindToLifecycle(any, any)) + .thenAnswer((_) => Future.value(mockCamera)); + when(mockCamera.getCameraInfo()) + .thenAnswer((_) => Future.value(MockCameraInfo())); final CameraImageData mockCameraImageData = MockCameraImageData(); final Stream imageStream = @@ -443,12 +912,14 @@ void main() { test( 'onStreamedFrameAvaiable returns stream that responds expectedly to being listened to', () async { - final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); + final FakeAndroidCameraCameraX camera = + FakeAndroidCameraCameraX(shouldCreateDetachedObjectForTesting: true); const int cameraId = 33; final ProcessCameraProvider mockProcessCameraProvider = MockProcessCameraProvider(); final CameraSelector mockCameraSelector = MockCameraSelector(); final Camera mockCamera = MockCamera(); + final CameraInfo mockCameraInfo = MockCameraInfo(); final MockImageProxy mockImageProxy = MockImageProxy(); final MockPlaneProxy mockPlane = MockPlaneProxy(); final List mockPlanes = [mockPlane]; @@ -461,11 +932,11 @@ void main() { camera.processCameraProvider = mockProcessCameraProvider; camera.cameraSelector = mockCameraSelector; - camera.createDetachedCallbacks = true; when(mockProcessCameraProvider.bindToLifecycle( mockCameraSelector, [camera.mockImageAnalysis])) .thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); when(mockImageProxy.getPlanes()) .thenAnswer((_) => Future>.value(mockPlanes)); when(mockPlane.buffer).thenReturn(buffer); @@ -475,18 +946,13 @@ void main() { when(mockImageProxy.height).thenReturn(imageHeight); when(mockImageProxy.width).thenReturn(imageWidth); + final Completer imageDataCompleter = + Completer(); 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)); + imageDataCompleter.complete(imageData); }); // Test ImageAnalysis use case is bound to ProcessCameraProvider. @@ -497,13 +963,28 @@ void main() { mockCameraSelector, [camera.mockImageAnalysis])); await capturedAnalyzer.analyze(mockImageProxy); + final CameraImageData imageData = await imageDataCompleter.future; + + // Test Analyzer correctly process ImageProxy instances. + expect(imageData.planes.length, equals(1)); + 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)); + + // Verify camera and cameraInfo were properly updated. + expect(camera.camera, equals(mockCamera)); + expect(camera.cameraInfo, equals(mockCameraInfo)); onStreamedFrameAvailableSubscription.cancel(); }); test( 'onStreamedFrameAvaiable returns stream that responds expectedly to being canceled', () async { - final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); + final FakeAndroidCameraCameraX camera = + FakeAndroidCameraCameraX(shouldCreateDetachedObjectForTesting: true); const int cameraId = 32; final ProcessCameraProvider mockProcessCameraProvider = MockProcessCameraProvider(); @@ -512,11 +993,11 @@ void main() { camera.processCameraProvider = mockProcessCameraProvider; camera.cameraSelector = mockCameraSelector; - camera.createDetachedCallbacks = true; when(mockProcessCameraProvider.bindToLifecycle( mockCameraSelector, [camera.mockImageAnalysis])) .thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => MockCameraInfo()); final StreamSubscription imageStreamSubscription = camera .onStreamedFrameAvailable(cameraId) @@ -531,9 +1012,11 @@ void main() { }); } -/// Mock of [AndroidCameraCameraX] that stubs behavior of some methods for -/// testing. -class MockAndroidCameraCameraX extends AndroidCameraCameraX { +/// Fake [AndroidCameraCameraX] that stubs behavior of some methods for testing. +class FakeAndroidCameraCameraX extends AndroidCameraCameraX { + FakeAndroidCameraCameraX({super.shouldCreateDetachedObjectForTesting}) + : super.forTesting(); + bool cameraPermissionsRequested = false; bool startedListeningForDeviceOrientationChanges = false; @@ -542,6 +1025,8 @@ class MockAndroidCameraCameraX extends AndroidCameraCameraX { final MockImageCapture testImageCapture = MockImageCapture(); final MockCameraSelector mockBackCameraSelector = MockCameraSelector(); final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); + final MockRecorder testRecorder = MockRecorder(); + final MockVideoCapture testVideoCapture = MockVideoCapture(); final MockImageAnalysis mockImageAnalysis = MockImageAnalysis(); @override @@ -578,6 +1063,16 @@ class MockAndroidCameraCameraX extends AndroidCameraCameraX { return testImageCapture; } + @override + Recorder createRecorder() { + return testRecorder; + } + + @override + Future createVideoCapture(Recorder recorder) { + return Future.value(testVideoCapture); + } + @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 56673fc45926..6a2aa4e080a7 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,30 +3,39 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i9; -import 'dart:typed_data' as _i16; - -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 'dart:async' as _i14; +import 'dart:typed_data' as _i23; + +import 'package:camera_android_camerax/src/analyzer.dart' as _i19; +import 'package:camera_android_camerax/src/camera.dart' as _i7; +import 'package:camera_android_camerax/src/camera_info.dart' as _i2; +import 'package:camera_android_camerax/src/camera_selector.dart' as _i17; +import 'package:camera_android_camerax/src/camera_state.dart' as _i15; +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i6; +import 'package:camera_android_camerax/src/exposure_state.dart' as _i4; +import 'package:camera_android_camerax/src/image_analysis.dart' as _i18; +import 'package:camera_android_camerax/src/image_capture.dart' as _i20; +import 'package:camera_android_camerax/src/image_proxy.dart' as _i21; +import 'package:camera_android_camerax/src/live_data.dart' as _i3; +import 'package:camera_android_camerax/src/observer.dart' as _i29; +import 'package:camera_android_camerax/src/pending_recording.dart' as _i8; +import 'package:camera_android_camerax/src/plane_proxy.dart' as _i22; +import 'package:camera_android_camerax/src/preview.dart' as _i24; import 'package:camera_android_camerax/src/process_camera_provider.dart' - as _i18; -import 'package:camera_android_camerax/src/use_case.dart' as _i19; + as _i25; +import 'package:camera_android_camerax/src/recorder.dart' as _i10; +import 'package:camera_android_camerax/src/recording.dart' as _i9; +import 'package:camera_android_camerax/src/use_case.dart' as _i26; +import 'package:camera_android_camerax/src/video_capture.dart' as _i27; +import 'package:camera_android_camerax/src/zoom_state.dart' as _i16; 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; + as _i5; +import 'package:flutter/foundation.dart' as _i13; +import 'package:flutter/services.dart' as _i12; +import 'package:flutter/widgets.dart' as _i11; import 'package:mockito/mockito.dart' as _i1; -import 'test_camerax_library.g.dart' as _i20; +import 'test_camerax_library.g.dart' as _i28; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -39,9 +48,8 @@ import 'test_camerax_library.g.dart' as _i20; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeCameraImageFormat_0 extends _i1.SmartFake - implements _i2.CameraImageFormat { - _FakeCameraImageFormat_0( +class _FakeCameraInfo_0 extends _i1.SmartFake implements _i2.CameraInfo { + _FakeCameraInfo_0( Object parent, Invocation parentInvocation, ) : super( @@ -50,9 +58,9 @@ class _FakeCameraImageFormat_0 extends _i1.SmartFake ); } -class _FakeResolutionInfo_1 extends _i1.SmartFake - implements _i3.ResolutionInfo { - _FakeResolutionInfo_1( +class _FakeLiveData_1 extends _i1.SmartFake + implements _i3.LiveData { + _FakeLiveData_1( Object parent, Invocation parentInvocation, ) : super( @@ -61,8 +69,8 @@ class _FakeResolutionInfo_1 extends _i1.SmartFake ); } -class _FakeCamera_2 extends _i1.SmartFake implements _i4.Camera { - _FakeCamera_2( +class _FakeExposureState_2 extends _i1.SmartFake implements _i4.ExposureState { + _FakeExposureState_2( Object parent, Invocation parentInvocation, ) : super( @@ -71,8 +79,82 @@ class _FakeCamera_2 extends _i1.SmartFake implements _i4.Camera { ); } -class _FakeWidget_3 extends _i1.SmartFake implements _i5.Widget { - _FakeWidget_3( +class _FakeCameraImageFormat_3 extends _i1.SmartFake + implements _i5.CameraImageFormat { + _FakeCameraImageFormat_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeExposureCompensationRange_4 extends _i1.SmartFake + implements _i6.ExposureCompensationRange { + _FakeExposureCompensationRange_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeResolutionInfo_5 extends _i1.SmartFake + implements _i6.ResolutionInfo { + _FakeResolutionInfo_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeCamera_6 extends _i1.SmartFake implements _i7.Camera { + _FakeCamera_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePendingRecording_7 extends _i1.SmartFake + implements _i8.PendingRecording { + _FakePendingRecording_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeRecording_8 extends _i1.SmartFake implements _i9.Recording { + _FakeRecording_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeRecorder_9 extends _i1.SmartFake implements _i10.Recorder { + _FakeRecorder_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWidget_10 extends _i1.SmartFake implements _i11.Widget { + _FakeWidget_10( Object parent, Invocation parentInvocation, ) : super( @@ -81,13 +163,14 @@ class _FakeWidget_3 extends _i1.SmartFake implements _i5.Widget { ); @override - String toString({_i6.DiagnosticLevel? minLevel = _i6.DiagnosticLevel.info}) => + String toString( + {_i12.DiagnosticLevel? minLevel = _i12.DiagnosticLevel.info}) => super.toString(); } -class _FakeInheritedWidget_4 extends _i1.SmartFake - implements _i5.InheritedWidget { - _FakeInheritedWidget_4( +class _FakeInheritedWidget_11 extends _i1.SmartFake + implements _i11.InheritedWidget { + _FakeInheritedWidget_11( Object parent, Invocation parentInvocation, ) : super( @@ -96,13 +179,14 @@ class _FakeInheritedWidget_4 extends _i1.SmartFake ); @override - String toString({_i6.DiagnosticLevel? minLevel = _i6.DiagnosticLevel.info}) => + String toString( + {_i12.DiagnosticLevel? minLevel = _i12.DiagnosticLevel.info}) => super.toString(); } -class _FakeDiagnosticsNode_5 extends _i1.SmartFake - implements _i7.DiagnosticsNode { - _FakeDiagnosticsNode_5( +class _FakeDiagnosticsNode_12 extends _i1.SmartFake + implements _i13.DiagnosticsNode { + _FakeDiagnosticsNode_12( Object parent, Invocation parentInvocation, ) : super( @@ -112,8 +196,8 @@ class _FakeDiagnosticsNode_5 extends _i1.SmartFake @override String toString({ - _i7.TextTreeConfiguration? parentConfiguration, - _i6.DiagnosticLevel? minLevel = _i6.DiagnosticLevel.info, + _i13.TextTreeConfiguration? parentConfiguration, + _i12.DiagnosticLevel? minLevel = _i12.DiagnosticLevel.info, }) => super.toString(); } @@ -121,40 +205,135 @@ class _FakeDiagnosticsNode_5 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 _i4.Camera {} +class MockCamera extends _i1.Mock implements _i7.Camera { + @override + _i14.Future<_i2.CameraInfo> getCameraInfo() => (super.noSuchMethod( + Invocation.method( + #getCameraInfo, + [], + ), + returnValue: _i14.Future<_i2.CameraInfo>.value(_FakeCameraInfo_0( + this, + Invocation.method( + #getCameraInfo, + [], + ), + )), + returnValueForMissingStub: + _i14.Future<_i2.CameraInfo>.value(_FakeCameraInfo_0( + this, + Invocation.method( + #getCameraInfo, + [], + ), + )), + ) as _i14.Future<_i2.CameraInfo>); +} /// A class which mocks [CameraInfo]. /// /// See the documentation for Mockito's code generation for more information. -class MockCameraInfo extends _i1.Mock implements _i8.CameraInfo { +class MockCameraInfo extends _i1.Mock implements _i2.CameraInfo { @override - _i9.Future getSensorRotationDegrees() => (super.noSuchMethod( + _i14.Future getSensorRotationDegrees() => (super.noSuchMethod( Invocation.method( #getSensorRotationDegrees, [], ), - returnValue: _i9.Future.value(0), - returnValueForMissingStub: _i9.Future.value(0), - ) as _i9.Future); + returnValue: _i14.Future.value(0), + returnValueForMissingStub: _i14.Future.value(0), + ) as _i14.Future); + @override + _i14.Future<_i3.LiveData<_i15.CameraState>> getCameraState() => + (super.noSuchMethod( + Invocation.method( + #getCameraState, + [], + ), + returnValue: _i14.Future<_i3.LiveData<_i15.CameraState>>.value( + _FakeLiveData_1<_i15.CameraState>( + this, + Invocation.method( + #getCameraState, + [], + ), + )), + returnValueForMissingStub: + _i14.Future<_i3.LiveData<_i15.CameraState>>.value( + _FakeLiveData_1<_i15.CameraState>( + this, + Invocation.method( + #getCameraState, + [], + ), + )), + ) as _i14.Future<_i3.LiveData<_i15.CameraState>>); + @override + _i14.Future<_i4.ExposureState> getExposureState() => (super.noSuchMethod( + Invocation.method( + #getExposureState, + [], + ), + returnValue: _i14.Future<_i4.ExposureState>.value(_FakeExposureState_2( + this, + Invocation.method( + #getExposureState, + [], + ), + )), + returnValueForMissingStub: + _i14.Future<_i4.ExposureState>.value(_FakeExposureState_2( + this, + Invocation.method( + #getExposureState, + [], + ), + )), + ) as _i14.Future<_i4.ExposureState>); + @override + _i14.Future<_i3.LiveData<_i16.ZoomState>> getZoomState() => + (super.noSuchMethod( + Invocation.method( + #getZoomState, + [], + ), + returnValue: _i14.Future<_i3.LiveData<_i16.ZoomState>>.value( + _FakeLiveData_1<_i16.ZoomState>( + this, + Invocation.method( + #getZoomState, + [], + ), + )), + returnValueForMissingStub: + _i14.Future<_i3.LiveData<_i16.ZoomState>>.value( + _FakeLiveData_1<_i16.ZoomState>( + this, + Invocation.method( + #getZoomState, + [], + ), + )), + ) as _i14.Future<_i3.LiveData<_i16.ZoomState>>); } /// 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 { +class MockCameraImageData extends _i1.Mock implements _i5.CameraImageData { @override - _i2.CameraImageFormat get format => (super.noSuchMethod( + _i5.CameraImageFormat get format => (super.noSuchMethod( Invocation.getter(#format), - returnValue: _FakeCameraImageFormat_0( + returnValue: _FakeCameraImageFormat_3( this, Invocation.getter(#format), ), - returnValueForMissingStub: _FakeCameraImageFormat_0( + returnValueForMissingStub: _FakeCameraImageFormat_3( this, Invocation.getter(#format), ), - ) as _i2.CameraImageFormat); + ) as _i5.CameraImageFormat); @override int get height => (super.noSuchMethod( Invocation.getter(#height), @@ -168,82 +347,108 @@ class MockCameraImageData extends _i1.Mock implements _i2.CameraImageData { returnValueForMissingStub: 0, ) as int); @override - List<_i2.CameraImagePlane> get planes => (super.noSuchMethod( + List<_i5.CameraImagePlane> get planes => (super.noSuchMethod( Invocation.getter(#planes), - returnValue: <_i2.CameraImagePlane>[], - returnValueForMissingStub: <_i2.CameraImagePlane>[], - ) as List<_i2.CameraImagePlane>); + returnValue: <_i5.CameraImagePlane>[], + returnValueForMissingStub: <_i5.CameraImagePlane>[], + ) as List<_i5.CameraImagePlane>); } /// A class which mocks [CameraSelector]. /// /// See the documentation for Mockito's code generation for more information. -class MockCameraSelector extends _i1.Mock implements _i10.CameraSelector { +class MockCameraSelector extends _i1.Mock implements _i17.CameraSelector { @override - _i9.Future> filter(List<_i8.CameraInfo>? cameraInfos) => + _i14.Future> filter(List<_i2.CameraInfo>? cameraInfos) => (super.noSuchMethod( Invocation.method( #filter, [cameraInfos], ), - returnValue: _i9.Future>.value(<_i8.CameraInfo>[]), + returnValue: + _i14.Future>.value(<_i2.CameraInfo>[]), returnValueForMissingStub: - _i9.Future>.value(<_i8.CameraInfo>[]), - ) as _i9.Future>); + _i14.Future>.value(<_i2.CameraInfo>[]), + ) as _i14.Future>); +} + +/// A class which mocks [ExposureState]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockExposureState extends _i1.Mock implements _i4.ExposureState { + @override + _i6.ExposureCompensationRange get exposureCompensationRange => + (super.noSuchMethod( + Invocation.getter(#exposureCompensationRange), + returnValue: _FakeExposureCompensationRange_4( + this, + Invocation.getter(#exposureCompensationRange), + ), + returnValueForMissingStub: _FakeExposureCompensationRange_4( + this, + Invocation.getter(#exposureCompensationRange), + ), + ) as _i6.ExposureCompensationRange); + @override + double get exposureCompensationStep => (super.noSuchMethod( + Invocation.getter(#exposureCompensationStep), + returnValue: 0.0, + returnValueForMissingStub: 0.0, + ) as double); } /// A class which mocks [ImageAnalysis]. /// /// See the documentation for Mockito's code generation for more information. -class MockImageAnalysis extends _i1.Mock implements _i11.ImageAnalysis { +class MockImageAnalysis extends _i1.Mock implements _i18.ImageAnalysis { @override - _i9.Future setAnalyzer(_i12.Analyzer? analyzer) => (super.noSuchMethod( + _i14.Future setAnalyzer(_i19.Analyzer? analyzer) => (super.noSuchMethod( Invocation.method( #setAnalyzer, [analyzer], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); @override - _i9.Future clearAnalyzer() => (super.noSuchMethod( + _i14.Future clearAnalyzer() => (super.noSuchMethod( Invocation.method( #clearAnalyzer, [], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); } /// A class which mocks [ImageCapture]. /// /// See the documentation for Mockito's code generation for more information. -class MockImageCapture extends _i1.Mock implements _i13.ImageCapture { +class MockImageCapture extends _i1.Mock implements _i20.ImageCapture { @override - _i9.Future setFlashMode(int? newFlashMode) => (super.noSuchMethod( + _i14.Future setFlashMode(int? newFlashMode) => (super.noSuchMethod( Invocation.method( #setFlashMode, [newFlashMode], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); @override - _i9.Future takePicture() => (super.noSuchMethod( + _i14.Future takePicture() => (super.noSuchMethod( Invocation.method( #takePicture, [], ), - returnValue: _i9.Future.value(''), - returnValueForMissingStub: _i9.Future.value(''), - ) as _i9.Future); + returnValue: _i14.Future.value(''), + returnValueForMissingStub: _i14.Future.value(''), + ) as _i14.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 { +class MockImageProxy extends _i1.Mock implements _i21.ImageProxy { @override int get format => (super.noSuchMethod( Invocation.getter(#format), @@ -263,37 +468,37 @@ class MockImageProxy extends _i1.Mock implements _i14.ImageProxy { returnValueForMissingStub: 0, ) as int); @override - _i9.Future> getPlanes() => (super.noSuchMethod( + _i14.Future> getPlanes() => (super.noSuchMethod( Invocation.method( #getPlanes, [], ), returnValue: - _i9.Future>.value(<_i15.PlaneProxy>[]), + _i14.Future>.value(<_i22.PlaneProxy>[]), returnValueForMissingStub: - _i9.Future>.value(<_i15.PlaneProxy>[]), - ) as _i9.Future>); + _i14.Future>.value(<_i22.PlaneProxy>[]), + ) as _i14.Future>); @override - _i9.Future close() => (super.noSuchMethod( + _i14.Future close() => (super.noSuchMethod( Invocation.method( #close, [], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.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 { +class MockPlaneProxy extends _i1.Mock implements _i22.PlaneProxy { @override - _i16.Uint8List get buffer => (super.noSuchMethod( + _i23.Uint8List get buffer => (super.noSuchMethod( Invocation.getter(#buffer), - returnValue: _i16.Uint8List(0), - returnValueForMissingStub: _i16.Uint8List(0), - ) as _i16.Uint8List); + returnValue: _i23.Uint8List(0), + returnValueForMissingStub: _i23.Uint8List(0), + ) as _i23.Uint8List); @override int get pixelStride => (super.noSuchMethod( Invocation.getter(#pixelStride), @@ -311,16 +516,16 @@ class MockPlaneProxy extends _i1.Mock implements _i15.PlaneProxy { /// A class which mocks [Preview]. /// /// See the documentation for Mockito's code generation for more information. -class MockPreview extends _i1.Mock implements _i17.Preview { +class MockPreview extends _i1.Mock implements _i24.Preview { @override - _i9.Future setSurfaceProvider() => (super.noSuchMethod( + _i14.Future setSurfaceProvider() => (super.noSuchMethod( Invocation.method( #setSurfaceProvider, [], ), - returnValue: _i9.Future.value(0), - returnValueForMissingStub: _i9.Future.value(0), - ) as _i9.Future); + returnValue: _i14.Future.value(0), + returnValueForMissingStub: _i14.Future.value(0), + ) as _i14.Future); @override void releaseFlutterSurfaceTexture() => super.noSuchMethod( Invocation.method( @@ -330,12 +535,13 @@ class MockPreview extends _i1.Mock implements _i17.Preview { returnValueForMissingStub: null, ); @override - _i9.Future<_i3.ResolutionInfo> getResolutionInfo() => (super.noSuchMethod( + _i14.Future<_i6.ResolutionInfo> getResolutionInfo() => (super.noSuchMethod( Invocation.method( #getResolutionInfo, [], ), - returnValue: _i9.Future<_i3.ResolutionInfo>.value(_FakeResolutionInfo_1( + returnValue: + _i14.Future<_i6.ResolutionInfo>.value(_FakeResolutionInfo_5( this, Invocation.method( #getResolutionInfo, @@ -343,36 +549,37 @@ class MockPreview extends _i1.Mock implements _i17.Preview { ), )), returnValueForMissingStub: - _i9.Future<_i3.ResolutionInfo>.value(_FakeResolutionInfo_1( + _i14.Future<_i6.ResolutionInfo>.value(_FakeResolutionInfo_5( this, Invocation.method( #getResolutionInfo, [], ), )), - ) as _i9.Future<_i3.ResolutionInfo>); + ) as _i14.Future<_i6.ResolutionInfo>); } /// A class which mocks [ProcessCameraProvider]. /// /// See the documentation for Mockito's code generation for more information. class MockProcessCameraProvider extends _i1.Mock - implements _i18.ProcessCameraProvider { + implements _i25.ProcessCameraProvider { @override - _i9.Future> getAvailableCameraInfos() => + _i14.Future> getAvailableCameraInfos() => (super.noSuchMethod( Invocation.method( #getAvailableCameraInfos, [], ), - returnValue: _i9.Future>.value(<_i8.CameraInfo>[]), + returnValue: + _i14.Future>.value(<_i2.CameraInfo>[]), returnValueForMissingStub: - _i9.Future>.value(<_i8.CameraInfo>[]), - ) as _i9.Future>); + _i14.Future>.value(<_i2.CameraInfo>[]), + ) as _i14.Future>); @override - _i9.Future<_i4.Camera> bindToLifecycle( - _i10.CameraSelector? cameraSelector, - List<_i19.UseCase>? useCases, + _i14.Future<_i7.Camera> bindToLifecycle( + _i17.CameraSelector? cameraSelector, + List<_i26.UseCase>? useCases, ) => (super.noSuchMethod( Invocation.method( @@ -382,7 +589,7 @@ class MockProcessCameraProvider extends _i1.Mock useCases, ], ), - returnValue: _i9.Future<_i4.Camera>.value(_FakeCamera_2( + returnValue: _i14.Future<_i7.Camera>.value(_FakeCamera_6( this, Invocation.method( #bindToLifecycle, @@ -392,7 +599,7 @@ class MockProcessCameraProvider extends _i1.Mock ], ), )), - returnValueForMissingStub: _i9.Future<_i4.Camera>.value(_FakeCamera_2( + returnValueForMissingStub: _i14.Future<_i7.Camera>.value(_FakeCamera_6( this, Invocation.method( #bindToLifecycle, @@ -402,18 +609,18 @@ class MockProcessCameraProvider extends _i1.Mock ], ), )), - ) as _i9.Future<_i4.Camera>); + ) as _i14.Future<_i7.Camera>); @override - _i9.Future isBound(_i19.UseCase? useCase) => (super.noSuchMethod( + _i14.Future isBound(_i26.UseCase? useCase) => (super.noSuchMethod( Invocation.method( #isBound, [useCase], ), - returnValue: _i9.Future.value(false), - returnValueForMissingStub: _i9.Future.value(false), - ) as _i9.Future); + returnValue: _i14.Future.value(false), + returnValueForMissingStub: _i14.Future.value(false), + ) as _i14.Future); @override - void unbind(List<_i19.UseCase>? useCases) => super.noSuchMethod( + void unbind(List<_i26.UseCase>? useCases) => super.noSuchMethod( Invocation.method( #unbind, [useCases], @@ -430,50 +637,165 @@ class MockProcessCameraProvider extends _i1.Mock ); } -/// A class which mocks [TestInstanceManagerHostApi]. +/// A class which mocks [Recorder]. /// /// See the documentation for Mockito's code generation for more information. -class MockTestInstanceManagerHostApi extends _i1.Mock - implements _i20.TestInstanceManagerHostApi { +class MockRecorder extends _i1.Mock implements _i10.Recorder { @override - void clear() => super.noSuchMethod( + _i14.Future<_i8.PendingRecording> prepareRecording(String? path) => + (super.noSuchMethod( Invocation.method( - #clear, + #prepareRecording, + [path], + ), + returnValue: + _i14.Future<_i8.PendingRecording>.value(_FakePendingRecording_7( + this, + Invocation.method( + #prepareRecording, + [path], + ), + )), + returnValueForMissingStub: + _i14.Future<_i8.PendingRecording>.value(_FakePendingRecording_7( + this, + Invocation.method( + #prepareRecording, + [path], + ), + )), + ) as _i14.Future<_i8.PendingRecording>); +} + +/// A class which mocks [PendingRecording]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPendingRecording extends _i1.Mock implements _i8.PendingRecording { + @override + _i14.Future<_i9.Recording> start() => (super.noSuchMethod( + Invocation.method( + #start, [], ), - returnValueForMissingStub: null, - ); + returnValue: _i14.Future<_i9.Recording>.value(_FakeRecording_8( + this, + Invocation.method( + #start, + [], + ), + )), + returnValueForMissingStub: + _i14.Future<_i9.Recording>.value(_FakeRecording_8( + this, + Invocation.method( + #start, + [], + ), + )), + ) as _i14.Future<_i9.Recording>); } -/// A class which mocks [BuildContext]. +/// A class which mocks [Recording]. /// /// See the documentation for Mockito's code generation for more information. -class MockBuildContext extends _i1.Mock implements _i5.BuildContext { - MockBuildContext() { - _i1.throwOnMissingStub(this); - } +class MockRecording extends _i1.Mock implements _i9.Recording { + @override + _i14.Future close() => (super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); + @override + _i14.Future pause() => (super.noSuchMethod( + Invocation.method( + #pause, + [], + ), + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); + @override + _i14.Future resume() => (super.noSuchMethod( + Invocation.method( + #resume, + [], + ), + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); + @override + _i14.Future stop() => (super.noSuchMethod( + Invocation.method( + #stop, + [], + ), + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); +} +/// A class which mocks [VideoCapture]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockVideoCapture extends _i1.Mock implements _i27.VideoCapture { @override - _i5.Widget get widget => (super.noSuchMethod( + _i14.Future<_i10.Recorder> getOutput() => (super.noSuchMethod( + Invocation.method( + #getOutput, + [], + ), + returnValue: _i14.Future<_i10.Recorder>.value(_FakeRecorder_9( + this, + Invocation.method( + #getOutput, + [], + ), + )), + returnValueForMissingStub: + _i14.Future<_i10.Recorder>.value(_FakeRecorder_9( + this, + Invocation.method( + #getOutput, + [], + ), + )), + ) as _i14.Future<_i10.Recorder>); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i11.BuildContext { + @override + _i11.Widget get widget => (super.noSuchMethod( Invocation.getter(#widget), - returnValue: _FakeWidget_3( + returnValue: _FakeWidget_10( + this, + Invocation.getter(#widget), + ), + returnValueForMissingStub: _FakeWidget_10( this, Invocation.getter(#widget), ), - ) as _i5.Widget); + ) as _i11.Widget); @override bool get mounted => (super.noSuchMethod( Invocation.getter(#mounted), returnValue: false, + returnValueForMissingStub: false, ) as bool); @override bool get debugDoingBuild => (super.noSuchMethod( Invocation.getter(#debugDoingBuild), returnValue: false, + returnValueForMissingStub: false, ) as bool); @override - _i5.InheritedWidget dependOnInheritedElement( - _i5.InheritedElement? ancestor, { + _i11.InheritedWidget dependOnInheritedElement( + _i11.InheritedElement? ancestor, { Object? aspect, }) => (super.noSuchMethod( @@ -482,7 +804,15 @@ class MockBuildContext extends _i1.Mock implements _i5.BuildContext { [ancestor], {#aspect: aspect}, ), - returnValue: _FakeInheritedWidget_4( + returnValue: _FakeInheritedWidget_11( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + returnValueForMissingStub: _FakeInheritedWidget_11( this, Invocation.method( #dependOnInheritedElement, @@ -490,9 +820,9 @@ class MockBuildContext extends _i1.Mock implements _i5.BuildContext { {#aspect: aspect}, ), ), - ) as _i5.InheritedWidget); + ) as _i11.InheritedWidget); @override - void visitAncestorElements(bool Function(_i5.Element)? visitor) => + void visitAncestorElements(bool Function(_i11.Element)? visitor) => super.noSuchMethod( Invocation.method( #visitAncestorElements, @@ -501,7 +831,7 @@ class MockBuildContext extends _i1.Mock implements _i5.BuildContext { returnValueForMissingStub: null, ); @override - void visitChildElements(_i5.ElementVisitor? visitor) => super.noSuchMethod( + void visitChildElements(_i11.ElementVisitor? visitor) => super.noSuchMethod( Invocation.method( #visitChildElements, [visitor], @@ -509,7 +839,7 @@ class MockBuildContext extends _i1.Mock implements _i5.BuildContext { returnValueForMissingStub: null, ); @override - void dispatchNotification(_i5.Notification? notification) => + void dispatchNotification(_i11.Notification? notification) => super.noSuchMethod( Invocation.method( #dispatchNotification, @@ -518,9 +848,9 @@ class MockBuildContext extends _i1.Mock implements _i5.BuildContext { returnValueForMissingStub: null, ); @override - _i7.DiagnosticsNode describeElement( + _i13.DiagnosticsNode describeElement( String? name, { - _i7.DiagnosticsTreeStyle? style = _i7.DiagnosticsTreeStyle.errorProperty, + _i13.DiagnosticsTreeStyle? style = _i13.DiagnosticsTreeStyle.errorProperty, }) => (super.noSuchMethod( Invocation.method( @@ -528,7 +858,15 @@ class MockBuildContext extends _i1.Mock implements _i5.BuildContext { [name], {#style: style}, ), - returnValue: _FakeDiagnosticsNode_5( + returnValue: _FakeDiagnosticsNode_12( + this, + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_12( this, Invocation.method( #describeElement, @@ -536,11 +874,11 @@ class MockBuildContext extends _i1.Mock implements _i5.BuildContext { {#style: style}, ), ), - ) as _i7.DiagnosticsNode); + ) as _i13.DiagnosticsNode); @override - _i7.DiagnosticsNode describeWidget( + _i13.DiagnosticsNode describeWidget( String? name, { - _i7.DiagnosticsTreeStyle? style = _i7.DiagnosticsTreeStyle.errorProperty, + _i13.DiagnosticsTreeStyle? style = _i13.DiagnosticsTreeStyle.errorProperty, }) => (super.noSuchMethod( Invocation.method( @@ -548,7 +886,15 @@ class MockBuildContext extends _i1.Mock implements _i5.BuildContext { [name], {#style: style}, ), - returnValue: _FakeDiagnosticsNode_5( + returnValue: _FakeDiagnosticsNode_12( + this, + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_12( this, Invocation.method( #describeWidget, @@ -556,9 +902,9 @@ class MockBuildContext extends _i1.Mock implements _i5.BuildContext { {#style: style}, ), ), - ) as _i7.DiagnosticsNode); + ) as _i13.DiagnosticsNode); @override - List<_i7.DiagnosticsNode> describeMissingAncestor( + List<_i13.DiagnosticsNode> describeMissingAncestor( {required Type? expectedAncestorType}) => (super.noSuchMethod( Invocation.method( @@ -566,21 +912,180 @@ class MockBuildContext extends _i1.Mock implements _i5.BuildContext { [], {#expectedAncestorType: expectedAncestorType}, ), - returnValue: <_i7.DiagnosticsNode>[], - ) as List<_i7.DiagnosticsNode>); + returnValue: <_i13.DiagnosticsNode>[], + returnValueForMissingStub: <_i13.DiagnosticsNode>[], + ) as List<_i13.DiagnosticsNode>); @override - _i7.DiagnosticsNode describeOwnershipChain(String? name) => + _i13.DiagnosticsNode describeOwnershipChain(String? name) => (super.noSuchMethod( Invocation.method( #describeOwnershipChain, [name], ), - returnValue: _FakeDiagnosticsNode_5( + returnValue: _FakeDiagnosticsNode_12( + this, + Invocation.method( + #describeOwnershipChain, + [name], + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_12( this, Invocation.method( #describeOwnershipChain, [name], ), ), - ) as _i7.DiagnosticsNode); + ) as _i13.DiagnosticsNode); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i28.TestInstanceManagerHostApi { + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [TestSystemServicesHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestSystemServicesHostApi extends _i1.Mock + implements _i28.TestSystemServicesHostApi { + @override + _i14.Future<_i6.CameraPermissionsErrorData?> requestCameraPermissions( + bool? enableAudio) => + (super.noSuchMethod( + Invocation.method( + #requestCameraPermissions, + [enableAudio], + ), + returnValue: _i14.Future<_i6.CameraPermissionsErrorData?>.value(), + returnValueForMissingStub: + _i14.Future<_i6.CameraPermissionsErrorData?>.value(), + ) as _i14.Future<_i6.CameraPermissionsErrorData?>); + @override + void startListeningForDeviceOrientationChange( + bool? isFrontFacing, + int? sensorOrientation, + ) => + super.noSuchMethod( + Invocation.method( + #startListeningForDeviceOrientationChange, + [ + isFrontFacing, + sensorOrientation, + ], + ), + returnValueForMissingStub: null, + ); + @override + void stopListeningForDeviceOrientationChange() => super.noSuchMethod( + Invocation.method( + #stopListeningForDeviceOrientationChange, + [], + ), + returnValueForMissingStub: null, + ); + @override + String getTempFilePath( + String? prefix, + String? suffix, + ) => + (super.noSuchMethod( + Invocation.method( + #getTempFilePath, + [ + prefix, + suffix, + ], + ), + returnValue: '', + returnValueForMissingStub: '', + ) as String); +} + +/// A class which mocks [ZoomState]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockZoomState extends _i1.Mock implements _i16.ZoomState { + @override + double get minZoomRatio => (super.noSuchMethod( + Invocation.getter(#minZoomRatio), + returnValue: 0.0, + returnValueForMissingStub: 0.0, + ) as double); + @override + double get maxZoomRatio => (super.noSuchMethod( + Invocation.getter(#maxZoomRatio), + returnValue: 0.0, + returnValueForMissingStub: 0.0, + ) as double); +} + +/// A class which mocks [LiveData]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLiveCameraState extends _i1.Mock + implements _i3.LiveData<_i15.CameraState> { + MockLiveCameraState() { + _i1.throwOnMissingStub(this); + } + + @override + _i14.Future observe(_i29.Observer<_i15.CameraState>? observer) => + (super.noSuchMethod( + Invocation.method( + #observe, + [observer], + ), + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); + @override + _i14.Future removeObservers() => (super.noSuchMethod( + Invocation.method( + #removeObservers, + [], + ), + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); +} + +/// A class which mocks [LiveData]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLiveZoomState extends _i1.Mock + implements _i3.LiveData<_i16.ZoomState> { + MockLiveZoomState() { + _i1.throwOnMissingStub(this); + } + + @override + _i14.Future observe(_i29.Observer<_i16.ZoomState>? observer) => + (super.noSuchMethod( + Invocation.method( + #observe, + [observer], + ), + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); + @override + _i14.Future removeObservers() => (super.noSuchMethod( + Invocation.method( + #removeObservers, + [], + ), + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); } diff --git a/packages/camera/camera_android_camerax/test/camera_info_test.dart b/packages/camera/camera_android_camerax/test/camera_info_test.dart index f0527bd1f944..487983b33656 100644 --- a/packages/camera/camera_android_camerax/test/camera_info_test.dart +++ b/packages/camera/camera_android_camerax/test/camera_info_test.dart @@ -3,8 +3,12 @@ // found in the LICENSE file. import 'package:camera_android_camerax/src/camera_info.dart'; +import 'package:camera_android_camerax/src/camera_state.dart'; import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/exposure_state.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/live_data.dart'; +import 'package:camera_android_camerax/src/zoom_state.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -12,7 +16,13 @@ import 'package:mockito/mockito.dart'; import 'camera_info_test.mocks.dart'; import 'test_camerax_library.g.dart'; -@GenerateMocks([TestCameraInfoHostApi, TestInstanceManagerHostApi]) +@GenerateMocks([ + TestCameraInfoHostApi, + TestInstanceManagerHostApi +], customMocks: >[ + MockSpec>(as: #MockLiveCameraState), + MockSpec>(as: #MockLiveZoomState), +]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -22,7 +32,9 @@ void main() { group('CameraInfo', () { tearDown(() => TestCameraInfoHostApi.setup(null)); - test('getSensorRotationDegreesTest', () async { + test( + 'getSensorRotationDegrees makes call to retrieve expected sensor rotation', + () async { final MockTestCameraInfoHostApi mockApi = MockTestCameraInfoHostApi(); TestCameraInfoHostApi.setup(mockApi); @@ -46,7 +58,110 @@ void main() { verify(mockApi.getSensorRotationDegrees(0)); }); - test('flutterApiCreateTest', () { + test('getCameraState makes call to retrieve live camera state', () async { + final MockTestCameraInfoHostApi mockApi = MockTestCameraInfoHostApi(); + TestCameraInfoHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final CameraInfo cameraInfo = CameraInfo.detached( + instanceManager: instanceManager, + ); + const int cameraIdentifier = 55; + final MockLiveCameraState mockLiveCameraState = MockLiveCameraState(); + const int liveCameraStateIdentifier = 73; + instanceManager.addHostCreatedInstance( + cameraInfo, + cameraIdentifier, + onCopy: (_) => CameraInfo.detached(), + ); + instanceManager.addHostCreatedInstance( + mockLiveCameraState, + liveCameraStateIdentifier, + onCopy: (_) => MockLiveCameraState(), + ); + + when(mockApi.getCameraState(cameraIdentifier)) + .thenReturn(liveCameraStateIdentifier); + + expect(await cameraInfo.getCameraState(), equals(mockLiveCameraState)); + verify(mockApi.getCameraState(cameraIdentifier)); + }); + + test('getExposureState makes call to retrieve expected ExposureState', + () async { + final MockTestCameraInfoHostApi mockApi = MockTestCameraInfoHostApi(); + TestCameraInfoHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final CameraInfo cameraInfo = CameraInfo.detached( + instanceManager: instanceManager, + ); + const int cameraInfoIdentifier = 4; + final ExposureState exposureState = ExposureState.detached( + exposureCompensationRange: + ExposureCompensationRange(maxCompensation: 0, minCompensation: 1), + exposureCompensationStep: 4, + instanceManager: instanceManager, + ); + const int exposureStateIdentifier = 45; + + instanceManager.addHostCreatedInstance( + cameraInfo, + cameraInfoIdentifier, + onCopy: (_) => CameraInfo.detached(), + ); + instanceManager.addHostCreatedInstance( + exposureState, + exposureStateIdentifier, + onCopy: (_) => ExposureState.detached( + exposureCompensationRange: ExposureCompensationRange( + maxCompensation: 0, minCompensation: 1), + exposureCompensationStep: 4), + ); + + when(mockApi.getExposureState(cameraInfoIdentifier)) + .thenReturn(exposureStateIdentifier); + expect(await cameraInfo.getExposureState(), equals(exposureState)); + + verify(mockApi.getExposureState(cameraInfoIdentifier)); + }); + + test('getZoomState makes call to retrieve expected ZoomState', () async { + final MockTestCameraInfoHostApi mockApi = MockTestCameraInfoHostApi(); + TestCameraInfoHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final CameraInfo cameraInfo = CameraInfo.detached( + instanceManager: instanceManager, + ); + const int cameraInfoIdentifier = 2; + final MockLiveZoomState mockLiveZoomState = MockLiveZoomState(); + const int mockLiveZoomStateIdentifier = 55; + + instanceManager.addHostCreatedInstance( + cameraInfo, + cameraInfoIdentifier, + onCopy: (_) => CameraInfo.detached(), + ); + instanceManager.addHostCreatedInstance( + mockLiveZoomState, mockLiveZoomStateIdentifier, + onCopy: (_) => MockLiveZoomState()); + + when(mockApi.getZoomState(cameraInfoIdentifier)) + .thenReturn(mockLiveZoomStateIdentifier); + expect(await cameraInfo.getZoomState(), equals(mockLiveZoomState)); + + verify(mockApi.getZoomState(cameraInfoIdentifier)); + }); + + test('flutterApi create makes call to create expected instance type', () { final InstanceManager instanceManager = InstanceManager( onWeakReferenceRemoved: (_) {}, ); diff --git a/packages/camera/camera_android_camerax/test/camera_info_test.mocks.dart b/packages/camera/camera_android_camerax/test/camera_info_test.mocks.dart index 3c153337bfa6..62483abcc5fc 100644 --- a/packages/camera/camera_android_camerax/test/camera_info_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/camera_info_test.mocks.dart @@ -3,6 +3,12 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:camera_android_camerax/src/camera_state.dart' as _i4; +import 'package:camera_android_camerax/src/live_data.dart' as _i3; +import 'package:camera_android_camerax/src/observer.dart' as _i6; +import 'package:camera_android_camerax/src/zoom_state.dart' as _i7; import 'package:mockito/mockito.dart' as _i1; import 'test_camerax_library.g.dart' as _i2; @@ -35,6 +41,30 @@ class MockTestCameraInfoHostApi extends _i1.Mock ), returnValue: 0, ) as int); + @override + int getCameraState(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getCameraState, + [identifier], + ), + returnValue: 0, + ) as int); + @override + int getExposureState(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getExposureState, + [identifier], + ), + returnValue: 0, + ) as int); + @override + int getZoomState(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getZoomState, + [identifier], + ), + returnValue: 0, + ) as int); } /// A class which mocks [TestInstanceManagerHostApi]. @@ -55,3 +85,63 @@ class MockTestInstanceManagerHostApi extends _i1.Mock returnValueForMissingStub: null, ); } + +/// A class which mocks [LiveData]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLiveCameraState extends _i1.Mock + implements _i3.LiveData<_i4.CameraState> { + MockLiveCameraState() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future observe(_i6.Observer<_i4.CameraState>? observer) => + (super.noSuchMethod( + Invocation.method( + #observe, + [observer], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObservers() => (super.noSuchMethod( + Invocation.method( + #removeObservers, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [LiveData]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLiveZoomState extends _i1.Mock + implements _i3.LiveData<_i7.ZoomState> { + MockLiveZoomState() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future observe(_i6.Observer<_i7.ZoomState>? observer) => + (super.noSuchMethod( + Invocation.method( + #observe, + [observer], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObservers() => (super.noSuchMethod( + Invocation.method( + #removeObservers, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} diff --git a/packages/camera/camera_android_camerax/test/camera_state_error_test.dart b/packages/camera/camera_android_camerax/test/camera_state_error_test.dart new file mode 100644 index 000000000000..48da38d7af3a --- /dev/null +++ b/packages/camera/camera_android_camerax/test/camera_state_error_test.dart @@ -0,0 +1,53 @@ +// 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/camera_state_error.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; + +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestInstanceManagerHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraStateError', () { + tearDown(() { + TestInstanceManagerHostApi.setup(null); + }); + + test( + 'FlutterAPI create makes call to create CameraStateError instance with expected identifier', + () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final CameraStateErrorFlutterApiImpl api = CameraStateErrorFlutterApiImpl( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + const int code = 23; + + api.create( + instanceIdentifier, + code, + ); + + // Test instance type. + final Object? instance = + instanceManager.getInstanceWithWeakReference(instanceIdentifier); + expect( + instance, + isA(), + ); + + // Test instance properties. + final CameraStateError cameraStateError = instance! as CameraStateError; + expect(cameraStateError.code, equals(code)); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/camera_state_error_test.mocks.dart b/packages/camera/camera_android_camerax/test/camera_state_error_test.mocks.dart new file mode 100644 index 000000000000..0590cd5b44d7 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/camera_state_error_test.mocks.dart @@ -0,0 +1,38 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/camera_state_error_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/camera_state_test.dart b/packages/camera/camera_android_camerax/test/camera_state_test.dart new file mode 100644 index 000000000000..3cc019c6548f --- /dev/null +++ b/packages/camera/camera_android_camerax/test/camera_state_test.dart @@ -0,0 +1,72 @@ +// 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/camera_state.dart'; +import 'package:camera_android_camerax/src/camera_state_error.dart'; +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; + +import 'camera_state_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('CameraState', () { + tearDown(() { + TestInstanceManagerHostApi.setup(null); + }); + + test( + 'FlutterAPI create makes call to create CameraState instance with expected identifier', + () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final CameraStateFlutterApiImpl api = CameraStateFlutterApiImpl( + instanceManager: instanceManager, + ); + + // Create CameraStateError for CameraState instance. + const int code = 23; + final CameraStateError cameraStateError = CameraStateError.detached( + instanceManager: instanceManager, + code: code, + ); + final int cameraStateErrorIdentifier = + instanceManager.addDartCreatedInstance(cameraStateError, onCopy: (_) { + return CameraStateError.detached(code: code); + }); + + // Create CameraState. + const int instanceIdentifier = 46; + const CameraStateType cameraStateType = CameraStateType.closed; + api.create( + instanceIdentifier, + CameraStateTypeData(value: cameraStateType), + cameraStateErrorIdentifier, + ); + + // Test instance type. + final Object? instance = + instanceManager.getInstanceWithWeakReference(instanceIdentifier); + expect( + instanceManager.getInstanceWithWeakReference(instanceIdentifier), + isA(), + ); + + // Test instance properties. + final CameraState cameraState = instance! as CameraState; + expect(cameraState.type, equals(cameraStateType)); + expect(cameraState.error, equals(cameraStateError)); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/camera_state_test.mocks.dart b/packages/camera/camera_android_camerax/test/camera_state_test.mocks.dart new file mode 100644 index 000000000000..3431bf69b8eb --- /dev/null +++ b/packages/camera/camera_android_camerax/test/camera_state_test.mocks.dart @@ -0,0 +1,38 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/camera_state_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/camera_test.dart b/packages/camera/camera_android_camerax/test/camera_test.dart index 5d08dd65c49c..05f5fe7dd029 100644 --- a/packages/camera/camera_android_camerax/test/camera_test.dart +++ b/packages/camera/camera_android_camerax/test/camera_test.dart @@ -3,14 +3,16 @@ // found in the LICENSE file. import 'package:camera_android_camerax/src/camera.dart'; +import 'package:camera_android_camerax/src/camera_info.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 'camera_test.mocks.dart'; import 'test_camerax_library.g.dart'; -@GenerateMocks([TestInstanceManagerHostApi]) +@GenerateMocks([TestCameraHostApi, TestInstanceManagerHostApi]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -18,7 +20,41 @@ void main() { TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); group('Camera', () { - test('flutterApiCreateTest', () { + tearDown(() => TestCameraHostApi.setup(null)); + + test('getCameraInfo makes call to retrieve expected CameraInfo', () async { + final MockTestCameraHostApi mockApi = MockTestCameraHostApi(); + TestCameraHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final Camera camera = Camera.detached( + instanceManager: instanceManager, + ); + const int cameraIdentifier = 24; + final CameraInfo cameraInfo = CameraInfo.detached(); + const int cameraInfoIdentifier = 88; + instanceManager.addHostCreatedInstance( + camera, + cameraIdentifier, + onCopy: (_) => Camera.detached(instanceManager: instanceManager), + ); + instanceManager.addHostCreatedInstance( + cameraInfo, + cameraInfoIdentifier, + onCopy: (_) => CameraInfo.detached(instanceManager: instanceManager), + ); + + when(mockApi.getCameraInfo(cameraIdentifier)) + .thenAnswer((_) => cameraInfoIdentifier); + + expect(await camera.getCameraInfo(), equals(cameraInfo)); + verify(mockApi.getCameraInfo(cameraIdentifier)); + }); + + test('flutterApiCreate makes call to add instance to instance manager', () { final InstanceManager instanceManager = InstanceManager( onWeakReferenceRemoved: (_) {}, ); diff --git a/packages/camera/camera_android_camerax/test/camera_test.mocks.dart b/packages/camera/camera_android_camerax/test/camera_test.mocks.dart index 915ba5584b4d..7f3af283f1a3 100644 --- a/packages/camera/camera_android_camerax/test/camera_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/camera_test.mocks.dart @@ -18,6 +18,24 @@ import 'test_camerax_library.g.dart' as _i2; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +/// A class which mocks [TestCameraHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestCameraHostApi extends _i1.Mock implements _i2.TestCameraHostApi { + MockTestCameraHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + int getCameraInfo(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getCameraInfo, + [identifier], + ), + returnValue: 0, + ) as int); +} + /// A class which mocks [TestInstanceManagerHostApi]. /// /// See the documentation for Mockito's code generation for more information. diff --git a/packages/camera/camera_android_camerax/test/exposure_state_test.dart b/packages/camera/camera_android_camerax/test/exposure_state_test.dart new file mode 100644 index 000000000000..fe1cf0a6261c --- /dev/null +++ b/packages/camera/camera_android_camerax/test/exposure_state_test.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. + +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/exposure_state.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; + +import 'exposure_state_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('ExposureState', () { + tearDown(() => TestCameraInfoHostApi.setup(null)); + + test('flutterApi create makes call to create expected ExposureState', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final ExposureStateFlutterApiImpl flutterApi = + ExposureStateFlutterApiImpl( + instanceManager: instanceManager, + ); + const int exposureStateIdentifier = 68; + final ExposureCompensationRange exposureCompensationRange = + ExposureCompensationRange(minCompensation: 5, maxCompensation: 7); + const double exposureCompensationStep = 0.3; + + flutterApi.create(exposureStateIdentifier, exposureCompensationRange, + exposureCompensationStep); + + final ExposureState instance = + instanceManager.getInstanceWithWeakReference(exposureStateIdentifier)! + as ExposureState; + expect(instance.exposureCompensationRange, + equals(exposureCompensationRange)); + expect( + instance.exposureCompensationStep, equals(exposureCompensationStep)); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/exposure_state_test.mocks.dart b/packages/camera/camera_android_camerax/test/exposure_state_test.mocks.dart new file mode 100644 index 000000000000..24445e698061 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/exposure_state_test.mocks.dart @@ -0,0 +1,38 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/exposure_state_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/live_data_test.dart b/packages/camera/camera_android_camerax/test/live_data_test.dart new file mode 100644 index 000000000000..95bf9a83315f --- /dev/null +++ b/packages/camera/camera_android_camerax/test/live_data_test.dart @@ -0,0 +1,164 @@ +// 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/camera_state.dart'; +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/live_data.dart'; +import 'package:camera_android_camerax/src/observer.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'live_data_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestLiveDataHostApi, TestInstanceManagerHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + group('LiveData', () { + tearDown(() { + TestLiveDataHostApi.setup(null); + TestInstanceManagerHostApi.setup(null); + }); + + test('observe makes call to add observer to LiveData instance', () async { + final MockTestLiveDataHostApi mockApi = MockTestLiveDataHostApi(); + TestLiveDataHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final LiveData instance = LiveData.detached( + instanceManager: instanceManager, + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance( + instance, + instanceIdentifier, + onCopy: (LiveData original) => LiveData.detached( + instanceManager: instanceManager, + ), + ); + + final Observer observer = Observer.detached( + instanceManager: instanceManager, + onChanged: (Object value) {}, + ); + const int observerIdentifier = 20; + instanceManager.addHostCreatedInstance( + observer, + observerIdentifier, + onCopy: (_) => Observer.detached( + instanceManager: instanceManager, + onChanged: (Object value) {}, + ), + ); + + await instance.observe( + observer, + ); + + verify(mockApi.observe( + instanceIdentifier, + observerIdentifier, + )); + }); + + test( + 'removeObservers makes call to remove observers from LiveData instance', + () async { + final MockTestLiveDataHostApi mockApi = MockTestLiveDataHostApi(); + TestLiveDataHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final LiveData instance = LiveData.detached( + instanceManager: instanceManager, + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance( + instance, + instanceIdentifier, + onCopy: (LiveData original) => LiveData.detached( + instanceManager: instanceManager, + ), + ); + + await instance.removeObservers(); + + verify(mockApi.removeObservers( + instanceIdentifier, + )); + }); + + test('getValue returns expected value', () async { + final MockTestLiveDataHostApi mockApi = MockTestLiveDataHostApi(); + TestLiveDataHostApi.setup(mockApi); + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final LiveData instance = LiveData.detached( + instanceManager: instanceManager, + ); + final CameraState testCameraState = + CameraState.detached(type: CameraStateType.closed); + const int instanceIdentifier = 0; + const int testCameraStateIdentifier = 22; + instanceManager.addHostCreatedInstance( + instance, + instanceIdentifier, + onCopy: (LiveData original) => + LiveData.detached( + instanceManager: instanceManager, + ), + ); + instanceManager.addHostCreatedInstance( + testCameraState, + testCameraStateIdentifier, + onCopy: (CameraState original) => CameraState.detached( + type: original.type, instanceManager: instanceManager), + ); + + when(mockApi.getValue(instanceIdentifier, any)) + .thenReturn(testCameraStateIdentifier); + + expect(await instance.getValue(), equals(testCameraState)); + }); + + test( + 'FlutterAPI create makes call to create LiveData instance with expected identifier', + () async { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final LiveDataFlutterApiImpl api = LiveDataFlutterApiImpl( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + + api.create( + instanceIdentifier, + LiveDataSupportedTypeData(value: LiveDataSupportedType.cameraState), + ); + + expect( + instanceManager.getInstanceWithWeakReference(instanceIdentifier), + isA>(), + ); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/live_data_test.mocks.dart b/packages/camera/camera_android_camerax/test/live_data_test.mocks.dart new file mode 100644 index 000000000000..01779862a3c6 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/live_data_test.mocks.dart @@ -0,0 +1,85 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/live_data_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 [TestLiveDataHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestLiveDataHostApi extends _i1.Mock + implements _i2.TestLiveDataHostApi { + MockTestLiveDataHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void observe( + int? identifier, + int? observerIdentifier, + ) => + super.noSuchMethod( + Invocation.method( + #observe, + [ + identifier, + observerIdentifier, + ], + ), + returnValueForMissingStub: null, + ); + @override + void removeObservers(int? identifier) => super.noSuchMethod( + Invocation.method( + #removeObservers, + [identifier], + ), + returnValueForMissingStub: null, + ); + @override + int? getValue( + int? identifier, + _i3.LiveDataSupportedTypeData? type, + ) => + (super.noSuchMethod(Invocation.method( + #getValue, + [ + identifier, + type, + ], + )) as int?); +} + +/// 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/observer_test.dart b/packages/camera/camera_android_camerax/test/observer_test.dart new file mode 100644 index 000000000000..41c3eba5598c --- /dev/null +++ b/packages/camera/camera_android_camerax/test/observer_test.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 'package:camera_android_camerax/src/camera_state.dart'; +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/observer.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'observer_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestObserverHostApi, TestInstanceManagerHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Observer', () { + tearDown(() { + TestObserverHostApi.setup(null); + TestInstanceManagerHostApi.setup(null); + }); + + test('HostApi create makes call to create Observer instance', () { + final MockTestObserverHostApi mockApi = MockTestObserverHostApi(); + TestObserverHostApi.setup(mockApi); + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final Observer instance = Observer( + instanceManager: instanceManager, + onChanged: (Object value) {}, + ); + + verify(mockApi.create( + instanceManager.getIdentifier(instance), + )); + }); + + test( + 'HostAPI create makes Observer instance that throws assertion error if onChanged receives unexpected parameter type', + () { + final MockTestObserverHostApi mockApi = MockTestObserverHostApi(); + TestObserverHostApi.setup(mockApi); + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + final Observer cameraStateObserver = + Observer.detached(onChanged: (Object value) {}); + + expect( + () => cameraStateObserver.onChanged( + CameraState.detached(type: CameraStateType.pendingOpen)), + throwsAssertionError); + }); + + test( + 'FlutterAPI onChanged makes call with expected parameter to Observer instance onChanged callback', + () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const int instanceIdentifier = 0; + late final Object? callbackParameter; + final Observer instance = Observer.detached( + onChanged: (Object value) { + callbackParameter = value; + }, + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance( + instance, + instanceIdentifier, + onCopy: (Observer original) => + Observer.detached( + onChanged: original.onChanged, + instanceManager: instanceManager, + ), + ); + + final ObserverFlutterApiImpl flutterApi = ObserverFlutterApiImpl( + instanceManager: instanceManager, + ); + + const CameraStateType cameraStateType = CameraStateType.closed; + + final CameraState value = CameraState.detached( + instanceManager: instanceManager, + type: cameraStateType, + ); + const int valueIdentifier = 11; + instanceManager.addHostCreatedInstance( + value, + valueIdentifier, + onCopy: (_) => CameraState.detached( + instanceManager: instanceManager, + type: cameraStateType, + ), + ); + + flutterApi.onChanged( + instanceIdentifier, + valueIdentifier, + ); + + expect(callbackParameter, value); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/observer_test.mocks.dart b/packages/camera/camera_android_camerax/test/observer_test.mocks.dart new file mode 100644 index 000000000000..9880bde0ae9a --- /dev/null +++ b/packages/camera/camera_android_camerax/test/observer_test.mocks.dart @@ -0,0 +1,57 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/observer_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 [TestObserverHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestObserverHostApi extends _i1.Mock + implements _i2.TestObserverHostApi { + MockTestObserverHostApi() { + _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/pending_recording_test.dart b/packages/camera/camera_android_camerax/test/pending_recording_test.dart new file mode 100644 index 000000000000..8957979cdd81 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/pending_recording_test.dart @@ -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. + +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/pending_recording.dart'; +import 'package:camera_android_camerax/src/recording.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'pending_recording_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks( + [TestPendingRecordingHostApi, TestInstanceManagerHostApi, Recording]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + test('start calls start on the Java side', () async { + final MockTestPendingRecordingHostApi mockApi = + MockTestPendingRecordingHostApi(); + TestPendingRecordingHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final PendingRecording pendingRecording = + PendingRecording.detached(instanceManager: instanceManager); + const int pendingRecordingId = 2; + instanceManager.addHostCreatedInstance(pendingRecording, pendingRecordingId, + onCopy: (_) => + PendingRecording.detached(instanceManager: instanceManager)); + + final Recording mockRecording = MockRecording(); + const int mockRecordingId = 3; + instanceManager.addHostCreatedInstance(mockRecording, mockRecordingId, + onCopy: (_) => Recording.detached(instanceManager: instanceManager)); + + when(mockApi.start(pendingRecordingId)).thenReturn(mockRecordingId); + expect(await pendingRecording.start(), mockRecording); + verify(mockApi.start(pendingRecordingId)); + }); + + test('flutterApiCreateTest', () async { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final PendingRecordingFlutterApi flutterApi = + PendingRecordingFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create(0); + + expect(instanceManager.getInstanceWithWeakReference(0), + isA()); + }); +} diff --git a/packages/camera/camera_android_camerax/test/pending_recording_test.mocks.dart b/packages/camera/camera_android_camerax/test/pending_recording_test.mocks.dart new file mode 100644 index 000000000000..95c3ad771152 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/pending_recording_test.mocks.dart @@ -0,0 +1,106 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/pending_recording_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i4; + +import 'package:camera_android_camerax/src/recording.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 [TestPendingRecordingHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestPendingRecordingHostApi extends _i1.Mock + implements _i2.TestPendingRecordingHostApi { + MockTestPendingRecordingHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + int start(int? identifier) => (super.noSuchMethod( + Invocation.method( + #start, + [identifier], + ), + returnValue: 0, + ) as int); +} + +/// 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, + ); +} + +/// A class which mocks [Recording]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockRecording extends _i1.Mock implements _i3.Recording { + MockRecording() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future close() => (super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future pause() => (super.noSuchMethod( + Invocation.method( + #pause, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future resume() => (super.noSuchMethod( + Invocation.method( + #resume, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future stop() => (super.noSuchMethod( + Invocation.method( + #stop, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); +} diff --git a/packages/camera/camera_android_camerax/test/preview_test.dart b/packages/camera/camera_android_camerax/test/preview_test.dart index c0d287ec0081..6eacd42b5caf 100644 --- a/packages/camera/camera_android_camerax/test/preview_test.dart +++ b/packages/camera/camera_android_camerax/test/preview_test.dart @@ -92,7 +92,7 @@ void main() { }); test( - 'releaseFlutterSurfaceTexture makes call to relase flutter surface texture entry', + 'releaseFlutterSurfaceTexture makes call to release flutter surface texture entry', () async { final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi(); TestPreviewHostApi.setup(mockApi); diff --git a/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart b/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart index f08fa8a466f9..a497956da5f4 100644 --- a/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart +++ b/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart @@ -195,7 +195,7 @@ void main() { verify(mockApi.unbind(0, [1])); }); - test('unbindAllTest', () async { + test('unbindAll unbinds UseCases', () async { final MockTestProcessCameraProviderHostApi mockApi = MockTestProcessCameraProviderHostApi(); TestProcessCameraProviderHostApi.setup(mockApi); @@ -207,22 +207,15 @@ void main() { ProcessCameraProvider.detached( instanceManager: instanceManager, ); - final UseCase fakeUseCase = - UseCase.detached(instanceManager: instanceManager); instanceManager.addHostCreatedInstance( processCameraProvider, 0, onCopy: (_) => ProcessCameraProvider.detached(), ); - instanceManager.addHostCreatedInstance( - fakeUseCase, - 1, - onCopy: (_) => UseCase.detached(), - ); - processCameraProvider.unbind([fakeUseCase]); - verify(mockApi.unbind(0, [1])); + processCameraProvider.unbindAll(); + verify(mockApi.unbindAll(0)); }); test('flutterApiCreateTest', () { diff --git a/packages/camera/camera_android_camerax/test/recorder_test.dart b/packages/camera/camera_android_camerax/test/recorder_test.dart new file mode 100644 index 000000000000..6dc398bbdad2 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/recorder_test.dart @@ -0,0 +1,113 @@ +// 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/instance_manager.dart'; +import 'package:camera_android_camerax/src/pending_recording.dart'; +import 'package:camera_android_camerax/src/recorder.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'recorder_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks( + [TestRecorderHostApi, TestInstanceManagerHostApi, PendingRecording]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + group('Recorder', () { + tearDown(() => TestCameraSelectorHostApi.setup(null)); + + test('detached create does not call create on the Java side', () async { + final MockTestRecorderHostApi mockApi = MockTestRecorderHostApi(); + TestRecorderHostApi.setup(mockApi); + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + Recorder.detached( + instanceManager: instanceManager, aspectRatio: 0, bitRate: 0); + + verifyNever(mockApi.create( + argThat(isA()), argThat(isA()), argThat(isA()))); + }); + + test('create does call create on the Java side', () async { + final MockTestRecorderHostApi mockApi = MockTestRecorderHostApi(); + TestRecorderHostApi.setup(mockApi); + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const int aspectRatio = 1; + const int bitRate = 2; + + Recorder( + instanceManager: instanceManager, + aspectRatio: aspectRatio, + bitRate: bitRate); + + verify(mockApi.create(argThat(isA()), aspectRatio, bitRate)); + }); + + test('prepareRecording calls prepareRecording on Java side', () async { + final MockTestRecorderHostApi mockApi = MockTestRecorderHostApi(); + TestRecorderHostApi.setup(mockApi); + when(mockApi.prepareRecording(0, '/test/path')).thenAnswer((_) => 2); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const String filePath = '/test/path'; + final Recorder recorder = + Recorder.detached(instanceManager: instanceManager); + const int recorderId = 0; + const int mockPendingRecordingId = 2; + + instanceManager.addHostCreatedInstance(recorder, recorderId, + onCopy: (_) => Recorder.detached(instanceManager: instanceManager)); + + final MockPendingRecording mockPendingRecording = MockPendingRecording(); + instanceManager.addHostCreatedInstance( + mockPendingRecording, mockPendingRecordingId, + onCopy: (_) => MockPendingRecording()); + when(mockApi.prepareRecording(recorderId, filePath)) + .thenReturn(mockPendingRecordingId); + final PendingRecording pendingRecording = + await recorder.prepareRecording(filePath); + expect(pendingRecording, mockPendingRecording); + }); + + test('flutterApiCreateTest', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final RecorderFlutterApiImpl flutterApi = RecorderFlutterApiImpl( + instanceManager: instanceManager, + ); + const int recorderId = 0; + const int aspectRatio = 1; + const int bitrate = 2; + + flutterApi.create(recorderId, aspectRatio, bitrate); + + expect(instanceManager.getInstanceWithWeakReference(recorderId), + isA()); + expect( + (instanceManager.getInstanceWithWeakReference(recorderId)! + as Recorder) + .aspectRatio, + equals(aspectRatio)); + expect( + (instanceManager.getInstanceWithWeakReference(0)! as Recorder) + .bitRate, + equals(bitrate)); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/recorder_test.mocks.dart b/packages/camera/camera_android_camerax/test/recorder_test.mocks.dart new file mode 100644 index 000000000000..10c709a057dc --- /dev/null +++ b/packages/camera/camera_android_camerax/test/recorder_test.mocks.dart @@ -0,0 +1,135 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/recorder_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:camera_android_camerax/src/pending_recording.dart' as _i4; +import 'package:camera_android_camerax/src/recording.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i3; + +// 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 + +class _FakeRecording_0 extends _i1.SmartFake implements _i2.Recording { + _FakeRecording_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [TestRecorderHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestRecorderHostApi extends _i1.Mock + implements _i3.TestRecorderHostApi { + MockTestRecorderHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create( + int? identifier, + int? aspectRatio, + int? bitRate, + ) => + super.noSuchMethod( + Invocation.method( + #create, + [ + identifier, + aspectRatio, + bitRate, + ], + ), + returnValueForMissingStub: null, + ); + @override + int getAspectRatio(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getAspectRatio, + [identifier], + ), + returnValue: 0, + ) as int); + @override + int getTargetVideoEncodingBitRate(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getTargetVideoEncodingBitRate, + [identifier], + ), + returnValue: 0, + ) as int); + @override + int prepareRecording( + int? identifier, + String? path, + ) => + (super.noSuchMethod( + Invocation.method( + #prepareRecording, + [ + identifier, + path, + ], + ), + returnValue: 0, + ) as int); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i3.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [PendingRecording]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPendingRecording extends _i1.Mock implements _i4.PendingRecording { + MockPendingRecording() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future<_i2.Recording> start() => (super.noSuchMethod( + Invocation.method( + #start, + [], + ), + returnValue: _i5.Future<_i2.Recording>.value(_FakeRecording_0( + this, + Invocation.method( + #start, + [], + ), + )), + ) as _i5.Future<_i2.Recording>); +} diff --git a/packages/camera/camera_android_camerax/test/recording_test.dart b/packages/camera/camera_android_camerax/test/recording_test.dart new file mode 100644 index 000000000000..06de01f42bcd --- /dev/null +++ b/packages/camera/camera_android_camerax/test/recording_test.dart @@ -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. + +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/recording.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'recording_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestRecordingHostApi, TestInstanceManagerHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + group('Recording', () { + tearDown(() => TestRecorderHostApi.setup(null)); + + test('close calls close on Java side', () async { + final MockTestRecordingHostApi mockApi = MockTestRecordingHostApi(); + TestRecordingHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final Recording recording = + Recording.detached(instanceManager: instanceManager); + const int recordingId = 0; + when(mockApi.close(recordingId)).thenAnswer((_) {}); + instanceManager.addHostCreatedInstance(recording, recordingId, + onCopy: (_) => Recording.detached(instanceManager: instanceManager)); + + recording.close(); + + verify(mockApi.close(recordingId)); + }); + + test('pause calls pause on Java side', () async { + final MockTestRecordingHostApi mockApi = MockTestRecordingHostApi(); + TestRecordingHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final Recording recording = + Recording.detached(instanceManager: instanceManager); + const int recordingId = 0; + when(mockApi.pause(recordingId)).thenAnswer((_) {}); + instanceManager.addHostCreatedInstance(recording, recordingId, + onCopy: (_) => Recording.detached(instanceManager: instanceManager)); + + recording.pause(); + + verify(mockApi.pause(recordingId)); + }); + + test('resume calls resume on Java side', () async { + final MockTestRecordingHostApi mockApi = MockTestRecordingHostApi(); + TestRecordingHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final Recording recording = + Recording.detached(instanceManager: instanceManager); + const int recordingId = 0; + when(mockApi.resume(recordingId)).thenAnswer((_) {}); + instanceManager.addHostCreatedInstance(recording, recordingId, + onCopy: (_) => Recording.detached(instanceManager: instanceManager)); + + recording.resume(); + + verify(mockApi.resume(recordingId)); + }); + + test('stop calls stop on Java side', () async { + final MockTestRecordingHostApi mockApi = MockTestRecordingHostApi(); + TestRecordingHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final Recording recording = + Recording.detached(instanceManager: instanceManager); + const int recordingId = 0; + when(mockApi.stop(recordingId)).thenAnswer((_) {}); + instanceManager.addHostCreatedInstance(recording, recordingId, + onCopy: (_) => Recording.detached(instanceManager: instanceManager)); + + recording.stop(); + + verify(mockApi.stop(recordingId)); + }); + + test('flutterApiCreateTest', () async { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final RecordingFlutterApi flutterApi = RecordingFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create(0); + + expect(instanceManager.getInstanceWithWeakReference(0), isA()); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/recording_test.mocks.dart b/packages/camera/camera_android_camerax/test/recording_test.mocks.dart new file mode 100644 index 000000000000..0334ae2833d0 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/recording_test.mocks.dart @@ -0,0 +1,81 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/recording_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 [TestRecordingHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestRecordingHostApi extends _i1.Mock + implements _i2.TestRecordingHostApi { + MockTestRecordingHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void close(int? identifier) => super.noSuchMethod( + Invocation.method( + #close, + [identifier], + ), + returnValueForMissingStub: null, + ); + @override + void pause(int? identifier) => super.noSuchMethod( + Invocation.method( + #pause, + [identifier], + ), + returnValueForMissingStub: null, + ); + @override + void resume(int? identifier) => super.noSuchMethod( + Invocation.method( + #resume, + [identifier], + ), + returnValueForMissingStub: null, + ); + @override + void stop(int? identifier) => super.noSuchMethod( + Invocation.method( + #stop, + [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/system_services_test.dart b/packages/camera/camera_android_camerax/test/system_services_test.dart index 0f0ecfd12ceb..414097b60152 100644 --- a/packages/camera/camera_android_camerax/test/system_services_test.dart +++ b/packages/camera/camera_android_camerax/test/system_services_test.dart @@ -109,5 +109,20 @@ void main() { }); SystemServicesFlutterApiImpl().onCameraError(testErrorDescription); }); + + test('getTempFilePath completes normally', () async { + final MockTestSystemServicesHostApi mockApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockApi); + const String testPath = '/test/path/'; + const String testPrefix = 'MOV'; + const String testSuffix = '.mp4'; + + when(mockApi.getTempFilePath(testPrefix, testSuffix)) + .thenReturn(testPath + testPrefix + testSuffix); + expect(await SystemServices.getTempFilePath(testPrefix, testSuffix), + testPath + testPrefix + testSuffix); + verify(mockApi.getTempFilePath(testPrefix, testSuffix)); + }); }); } diff --git a/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart b/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart index cb0acdaba0ba..f268a350bb38 100644 --- a/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart @@ -82,4 +82,19 @@ class MockTestSystemServicesHostApi extends _i1.Mock ), returnValueForMissingStub: null, ); + @override + String getTempFilePath( + String? prefix, + String? suffix, + ) => + (super.noSuchMethod( + Invocation.method( + #getTempFilePath, + [ + prefix, + suffix, + ], + ), + returnValue: '', + ) as String); } 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 0af4cd0029f8..2c4afd19b26b 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.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 @@ -86,6 +86,12 @@ abstract class TestCameraInfoHostApi { int getSensorRotationDegrees(int identifier); + int getCameraState(int identifier); + + int getExposureState(int identifier); + + int getZoomState(int identifier); + static void setup(TestCameraInfoHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -111,6 +117,72 @@ abstract class TestCameraInfoHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraInfoHostApi.getCameraState', 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.CameraInfoHostApi.getCameraState 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.CameraInfoHostApi.getCameraState was null, expected non-null int.'); + final int output = api.getCameraState(arg_identifier!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraInfoHostApi.getExposureState', 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.CameraInfoHostApi.getExposureState 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.CameraInfoHostApi.getExposureState was null, expected non-null int.'); + final int output = api.getExposureState(arg_identifier!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraInfoHostApi.getZoomState', 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.CameraInfoHostApi.getZoomState 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.CameraInfoHostApi.getZoomState was null, expected non-null int.'); + final int output = api.getZoomState(arg_identifier!); + return [output]; + }); + } + } } } @@ -347,6 +419,40 @@ abstract class TestProcessCameraProviderHostApi { } } +abstract class TestCameraHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + int getCameraInfo(int identifier); + + static void setup(TestCameraHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraHostApi.getCameraInfo', 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.CameraHostApi.getCameraInfo 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.CameraHostApi.getCameraInfo was null, expected non-null int.'); + final int output = api.getCameraInfo(arg_identifier!); + return [output]; + }); + } + } + } +} + class _TestSystemServicesHostApiCodec extends StandardMessageCodec { const _TestSystemServicesHostApiCodec(); @override @@ -383,6 +489,8 @@ abstract class TestSystemServicesHostApi { void stopListeningForDeviceOrientationChange(); + String getTempFilePath(String prefix, String suffix); + static void setup(TestSystemServicesHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -454,6 +562,31 @@ abstract class TestSystemServicesHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath', 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.SystemServicesHostApi.getTempFilePath was null.'); + final List args = (message as List?)!; + final String? arg_prefix = (args[0] as String?); + assert(arg_prefix != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath was null, expected non-null String.'); + final String? arg_suffix = (args[1] as String?); + assert(arg_suffix != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath was null, expected non-null String.'); + final String output = api.getTempFilePath(arg_prefix!, arg_suffix!); + return [output]; + }); + } + } } } @@ -590,6 +723,316 @@ abstract class TestPreviewHostApi { } } +abstract class TestVideoCaptureHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + int withOutput(int videoOutputId); + + int getOutput(int identifier); + + static void setup(TestVideoCaptureHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoCaptureHostApi.withOutput', 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.VideoCaptureHostApi.withOutput was null.'); + final List args = (message as List?)!; + final int? arg_videoOutputId = (args[0] as int?); + assert(arg_videoOutputId != null, + 'Argument for dev.flutter.pigeon.VideoCaptureHostApi.withOutput was null, expected non-null int.'); + final int output = api.withOutput(arg_videoOutputId!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoCaptureHostApi.getOutput', 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.VideoCaptureHostApi.getOutput 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.VideoCaptureHostApi.getOutput was null, expected non-null int.'); + final int output = api.getOutput(arg_identifier!); + return [output]; + }); + } + } + } +} + +abstract class TestRecorderHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier, int? aspectRatio, int? bitRate); + + int getAspectRatio(int identifier); + + int getTargetVideoEncodingBitRate(int identifier); + + int prepareRecording(int identifier, String path); + + static void setup(TestRecorderHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.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.RecorderHostApi.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.RecorderHostApi.create was null, expected non-null int.'); + final int? arg_aspectRatio = (args[1] as int?); + final int? arg_bitRate = (args[2] as int?); + api.create(arg_identifier!, arg_aspectRatio, arg_bitRate); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.getAspectRatio', 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.RecorderHostApi.getAspectRatio 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.RecorderHostApi.getAspectRatio was null, expected non-null int.'); + final int output = api.getAspectRatio(arg_identifier!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.getTargetVideoEncodingBitRate', + 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.RecorderHostApi.getTargetVideoEncodingBitRate 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.RecorderHostApi.getTargetVideoEncodingBitRate was null, expected non-null int.'); + final int output = api.getTargetVideoEncodingBitRate(arg_identifier!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.prepareRecording', 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.RecorderHostApi.prepareRecording 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.RecorderHostApi.prepareRecording was null, expected non-null int.'); + final String? arg_path = (args[1] as String?); + assert(arg_path != null, + 'Argument for dev.flutter.pigeon.RecorderHostApi.prepareRecording was null, expected non-null String.'); + final int output = api.prepareRecording(arg_identifier!, arg_path!); + return [output]; + }); + } + } + } +} + +abstract class TestPendingRecordingHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + int start(int identifier); + + static void setup(TestPendingRecordingHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PendingRecordingHostApi.start', 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.PendingRecordingHostApi.start 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.PendingRecordingHostApi.start was null, expected non-null int.'); + final int output = api.start(arg_identifier!); + return [output]; + }); + } + } + } +} + +abstract class TestRecordingHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + void close(int identifier); + + void pause(int identifier); + + void resume(int identifier); + + void stop(int identifier); + + static void setup(TestRecordingHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.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.RecordingHostApi.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.RecordingHostApi.close was null, expected non-null int.'); + api.close(arg_identifier!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.pause', 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.RecordingHostApi.pause 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.RecordingHostApi.pause was null, expected non-null int.'); + api.pause(arg_identifier!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.resume', 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.RecordingHostApi.resume 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.RecordingHostApi.resume was null, expected non-null int.'); + api.resume(arg_identifier!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.stop', 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.RecordingHostApi.stop 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.RecordingHostApi.stop was null, expected non-null int.'); + api.stop(arg_identifier!); + return []; + }); + } + } + } +} + class _TestImageCaptureHostApiCodec extends StandardMessageCodec { const _TestImageCaptureHostApiCodec(); @override @@ -845,6 +1288,152 @@ abstract class TestAnalyzerHostApi { } } +abstract class TestObserverHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier); + + static void setup(TestObserverHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ObserverHostApi.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.ObserverHostApi.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.ObserverHostApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return []; + }); + } + } + } +} + +class _TestLiveDataHostApiCodec extends StandardMessageCodec { + const _TestLiveDataHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is LiveDataSupportedTypeData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return LiveDataSupportedTypeData.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestLiveDataHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = _TestLiveDataHostApiCodec(); + + void observe(int identifier, int observerIdentifier); + + void removeObservers(int identifier); + + int? getValue(int identifier, LiveDataSupportedTypeData type); + + static void setup(TestLiveDataHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LiveDataHostApi.observe', 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.LiveDataHostApi.observe 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.LiveDataHostApi.observe was null, expected non-null int.'); + final int? arg_observerIdentifier = (args[1] as int?); + assert(arg_observerIdentifier != null, + 'Argument for dev.flutter.pigeon.LiveDataHostApi.observe was null, expected non-null int.'); + api.observe(arg_identifier!, arg_observerIdentifier!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LiveDataHostApi.removeObservers', 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.LiveDataHostApi.removeObservers 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.LiveDataHostApi.removeObservers was null, expected non-null int.'); + api.removeObservers(arg_identifier!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LiveDataHostApi.getValue', 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.LiveDataHostApi.getValue 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.LiveDataHostApi.getValue was null, expected non-null int.'); + final LiveDataSupportedTypeData? arg_type = + (args[1] as LiveDataSupportedTypeData?); + assert(arg_type != null, + 'Argument for dev.flutter.pigeon.LiveDataHostApi.getValue was null, expected non-null LiveDataSupportedTypeData.'); + final int? output = api.getValue(arg_identifier!, arg_type!); + return [output]; + }); + } + } + } +} + abstract class TestImageProxyHostApi { static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => TestDefaultBinaryMessengerBinding.instance; diff --git a/packages/camera/camera_android_camerax/test/video_capture_test.dart b/packages/camera/camera_android_camerax/test/video_capture_test.dart new file mode 100644 index 000000000000..560c48ffd592 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/video_capture_test.dart @@ -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. + +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/recorder.dart'; +import 'package:camera_android_camerax/src/video_capture.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'test_camerax_library.g.dart'; +import 'video_capture_test.mocks.dart'; + +@GenerateMocks( + [TestVideoCaptureHostApi, TestInstanceManagerHostApi, Recorder]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + test('withOutput calls the Java side and returns correct video capture', + () async { + final MockTestVideoCaptureHostApi mockApi = MockTestVideoCaptureHostApi(); + TestVideoCaptureHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final Recorder mockRecorder = MockRecorder(); + const int mockRecorderId = 2; + instanceManager.addHostCreatedInstance(mockRecorder, mockRecorderId, + onCopy: (_) => MockRecorder()); + + final VideoCapture videoCapture = + VideoCapture.detached(instanceManager: instanceManager); + const int videoCaptureId = 3; + instanceManager.addHostCreatedInstance(videoCapture, videoCaptureId, + onCopy: (_) => VideoCapture.detached(instanceManager: instanceManager)); + + when(mockApi.withOutput(mockRecorderId)).thenReturn(videoCaptureId); + + expect( + await VideoCapture.withOutput(mockRecorder, + instanceManager: instanceManager), + videoCapture); + verify(mockApi.withOutput(mockRecorderId)); + }); + + test('getOutput calls the Java side and returns correct Recorder', () async { + final MockTestVideoCaptureHostApi mockApi = MockTestVideoCaptureHostApi(); + TestVideoCaptureHostApi.setup(mockApi); + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final VideoCapture videoCapture = + VideoCapture.detached(instanceManager: instanceManager); + const int videoCaptureId = 2; + instanceManager.addHostCreatedInstance(videoCapture, videoCaptureId, + onCopy: (_) => VideoCapture.detached(instanceManager: instanceManager)); + + final Recorder mockRecorder = MockRecorder(); + const int mockRecorderId = 3; + instanceManager.addHostCreatedInstance(mockRecorder, mockRecorderId, + onCopy: (_) => Recorder.detached(instanceManager: instanceManager)); + + when(mockApi.getOutput(videoCaptureId)).thenReturn(mockRecorderId); + expect(await videoCapture.getOutput(), mockRecorder); + verify(mockApi.getOutput(videoCaptureId)); + }); + + test('flutterApiCreateTest', () async { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final VideoCaptureFlutterApi flutterApi = VideoCaptureFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create(0); + + expect( + instanceManager.getInstanceWithWeakReference(0), isA()); + }); +} diff --git a/packages/camera/camera_android_camerax/test/video_capture_test.mocks.dart b/packages/camera/camera_android_camerax/test/video_capture_test.mocks.dart new file mode 100644 index 000000000000..a8740b3d1a40 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/video_capture_test.mocks.dart @@ -0,0 +1,106 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/video_capture_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:camera_android_camerax/src/pending_recording.dart' as _i2; +import 'package:camera_android_camerax/src/recorder.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i3; + +// 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 + +class _FakePendingRecording_0 extends _i1.SmartFake + implements _i2.PendingRecording { + _FakePendingRecording_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [TestVideoCaptureHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestVideoCaptureHostApi extends _i1.Mock + implements _i3.TestVideoCaptureHostApi { + MockTestVideoCaptureHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + int withOutput(int? videoOutputId) => (super.noSuchMethod( + Invocation.method( + #withOutput, + [videoOutputId], + ), + returnValue: 0, + ) as int); + @override + int getOutput(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getOutput, + [identifier], + ), + returnValue: 0, + ) as int); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i3.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [Recorder]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockRecorder extends _i1.Mock implements _i4.Recorder { + MockRecorder() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future<_i2.PendingRecording> prepareRecording(String? path) => + (super.noSuchMethod( + Invocation.method( + #prepareRecording, + [path], + ), + returnValue: + _i5.Future<_i2.PendingRecording>.value(_FakePendingRecording_0( + this, + Invocation.method( + #prepareRecording, + [path], + ), + )), + ) as _i5.Future<_i2.PendingRecording>); +} diff --git a/packages/camera/camera_android_camerax/test/zoom_state_test.dart b/packages/camera/camera_android_camerax/test/zoom_state_test.dart new file mode 100644 index 000000000000..60ea85b5a221 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/zoom_state_test.dart @@ -0,0 +1,42 @@ +// 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/instance_manager.dart'; +import 'package:camera_android_camerax/src/zoom_state.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; + +import 'test_camerax_library.g.dart'; +import 'zoom_state_test.mocks.dart'; + +@GenerateMocks([TestInstanceManagerHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + group('ZoomState', () { + tearDown(() => TestInstanceManagerHostApi.setup(null)); + + test('flutterApi create makes call to create expected ZoomState', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final ZoomStateFlutterApiImpl flutterApi = ZoomStateFlutterApiImpl( + instanceManager: instanceManager, + ); + const int zoomStateIdentifier = 68; + const double minZoomRatio = 0; + const double maxZoomRatio = 1; + + flutterApi.create(zoomStateIdentifier, minZoomRatio, maxZoomRatio); + + final ZoomState instance = instanceManager + .getInstanceWithWeakReference(zoomStateIdentifier)! as ZoomState; + expect(instance.minZoomRatio, equals(minZoomRatio)); + expect(instance.maxZoomRatio, equals(maxZoomRatio)); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/zoom_state_test.mocks.dart b/packages/camera/camera_android_camerax/test/zoom_state_test.mocks.dart new file mode 100644 index 000000000000..cb52f890e53b --- /dev/null +++ b/packages/camera/camera_android_camerax/test/zoom_state_test.mocks.dart @@ -0,0 +1,38 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/zoom_state_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_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index a3e714fc33dd..4a4be5f17c3b 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.13+2 + +* Removes obsolete null checks on non-nullable values. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 0.9.13+1 * Clarifies explanation of endorsement in README. diff --git a/packages/camera/camera_avfoundation/example/lib/main.dart b/packages/camera/camera_avfoundation/example/lib/main.dart index dd02be3d7ae6..5162d3a22cfb 100644 --- a/packages/camera/camera_avfoundation/example/lib/main.dart +++ b/packages/camera/camera_avfoundation/example/lib/main.dart @@ -253,9 +253,7 @@ class _CameraExampleHomeState extends State child: Center( child: AspectRatio( aspectRatio: - localVideoController.value.size != null - ? localVideoController.value.aspectRatio - : 1.0, + localVideoController.value.aspectRatio, child: VideoPlayer(localVideoController)), ), ), @@ -1008,7 +1006,7 @@ class _CameraExampleHomeState extends State : VideoPlayerController.file(File(videoFile!.path)); videoPlayerListener = () { - if (videoController != null && videoController!.value.size != null) { + if (videoController != null) { // Refreshing the state to update video player with the correct ratio. if (mounted) { setState(() {}); diff --git a/packages/camera/camera_avfoundation/example/pubspec.yaml b/packages/camera/camera_avfoundation/example/pubspec.yaml index 3879cff75582..8468012b851b 100644 --- a/packages/camera/camera_avfoundation/example/pubspec.yaml +++ b/packages/camera/camera_avfoundation/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the camera plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: camera_avfoundation: diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index 55751196011c..937a1c2ae030 100644 --- a/packages/camera/camera_avfoundation/pubspec.yaml +++ b/packages/camera/camera_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_avfoundation description: iOS implementation of the camera plugin. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.13+1 +version: 0.9.13+2 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index e0736cac6a04..7b21165d8646 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.5.1 + +* Removes obsolete null checks on non-nullable values. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.5.0 * Adds NV21 as an image stream format (suitable for Android). diff --git a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart index 95f61a97a2c8..1c76c341d9b1 100644 --- a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart +++ b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart @@ -27,7 +27,7 @@ abstract class CameraEvent { /// Build a Camera Event, that relates a `cameraId`. /// /// The `cameraId` is the ID of the camera that triggered the event. - const CameraEvent(this.cameraId) : assert(cameraId != null); + const CameraEvent(this.cameraId); /// The ID of the Camera this event is associated to. final int cameraId; diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index 7c3da2d4fc85..3dc49cec038e 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/packages/tree/main/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%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.5.0 +version: 2.5.1 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: cross_file: ^0.3.1 diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index 5645d1291577..1d736b0ac541 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.3.1+4 + +* Removes obsolete null checks on non-nullable values. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 0.3.1+3 * Clarifies explanation of endorsement in README. diff --git a/packages/camera/camera_web/example/pubspec.yaml b/packages/camera/camera_web/example/pubspec.yaml index 222ba25cf5a5..f608af109c2e 100644 --- a/packages/camera/camera_web/example/pubspec.yaml +++ b/packages/camera/camera_web/example/pubspec.yaml @@ -2,8 +2,8 @@ name: camera_web_integration_tests publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 13ef21b1ea46..79bacadcebdc 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -471,9 +471,7 @@ class Camera { _onVideoRecordingErrorSubscription = mediaRecorder!.onError.listen((html.Event event) { final html.ErrorEvent error = event as html.ErrorEvent; - if (error != null) { - videoRecordingErrorController.add(error); - } + videoRecordingErrorController.add(error); }); if (maxVideoDuration != null) { diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index 9d6739d7ccbf..382444d3624b 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -2,11 +2,11 @@ name: camera_web description: A Flutter plugin for getting information about and controlling the camera on Web. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.3.1+3 +version: 0.3.1+4 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/camera/camera_windows/CHANGELOG.md b/packages/camera/camera_windows/CHANGELOG.md index 591bec687431..65777d3c40f3 100644 --- a/packages/camera/camera_windows/CHANGELOG.md +++ b/packages/camera/camera_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 0.2.1+6 * Sets a cmake_policy compatibility version to fix build warnings. diff --git a/packages/camera/camera_windows/example/pubspec.yaml b/packages/camera/camera_windows/example/pubspec.yaml index 5dc3a68a381d..6eb50aafd75f 100644 --- a/packages/camera/camera_windows/example/pubspec.yaml +++ b/packages/camera/camera_windows/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the camera_windows plugin. publish_to: 'none' environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: camera_platform_interface: ^2.1.2 diff --git a/packages/camera/camera_windows/pubspec.yaml b/packages/camera/camera_windows/pubspec.yaml index 81622a2b9a93..c62cd5d4914f 100644 --- a/packages/camera/camera_windows/pubspec.yaml +++ b/packages/camera/camera_windows/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 0.2.1+6 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/cross_file/CHANGELOG.md b/packages/cross_file/CHANGELOG.md index fb2efd3f1c1d..8e519f7c91c3 100644 --- a/packages/cross_file/CHANGELOG.md +++ b/packages/cross_file/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 0.3.3+4 * Reverts an accidental change in a constructor argument's nullability. diff --git a/packages/cross_file/pubspec.yaml b/packages/cross_file/pubspec.yaml index 45a81f71dd75..51a4e442f1b6 100644 --- a/packages/cross_file/pubspec.yaml +++ b/packages/cross_file/pubspec.yaml @@ -5,7 +5,7 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 0.3.3+4 environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=2.18.0 <4.0.0" dependencies: js: ^0.6.3 diff --git a/packages/css_colors/CHANGELOG.md b/packages/css_colors/CHANGELOG.md index 8d87d6361dec..c8d9f278223e 100644 --- a/packages/css_colors/CHANGELOG.md +++ b/packages/css_colors/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +- Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. - Aligns Dart and Flutter SDK constraints. - Updates minimum Flutter version to 3.0. - Updates package description. diff --git a/packages/css_colors/pubspec.yaml b/packages/css_colors/pubspec.yaml index eb32864bf14e..a2eca725ebba 100644 --- a/packages/css_colors/pubspec.yaml +++ b/packages/css_colors/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 1.1.1 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/dynamic_layouts/CHANGELOG.md b/packages/dynamic_layouts/CHANGELOG.md index 77830d1bdf2c..2c71313585d2 100644 --- a/packages/dynamic_layouts/CHANGELOG.md +++ b/packages/dynamic_layouts/CHANGELOG.md @@ -1,5 +1,7 @@ -## NEXT +## 0.0.1+1 +* Removes obsolete null checks on non-nullable values. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. * Aligns Dart and Flutter SDK constraints. * Updates minimum Flutter version to 3.0. diff --git a/packages/dynamic_layouts/example/.pluginToolsConfig.yaml b/packages/dynamic_layouts/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/dynamic_layouts/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/dynamic_layouts/example/android/build.gradle b/packages/dynamic_layouts/example/android/build.gradle index 4b30292ebe1f..ce647a433bd0 100644 --- a/packages/dynamic_layouts/example/android/build.gradle +++ b/packages/dynamic_layouts/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.21' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() diff --git a/packages/dynamic_layouts/example/ios/Flutter/AppFrameworkInfo.plist b/packages/dynamic_layouts/example/ios/Flutter/AppFrameworkInfo.plist index 8d4492f977ad..9625e105df39 100644 --- a/packages/dynamic_layouts/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/dynamic_layouts/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/dynamic_layouts/example/ios/Runner.xcodeproj/project.pbxproj b/packages/dynamic_layouts/example/ios/Runner.xcodeproj/project.pbxproj index 6edd238e7c68..fe3d67b222fe 100644 --- a/packages/dynamic_layouts/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/dynamic_layouts/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -171,6 +171,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -185,6 +186,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -272,7 +274,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -349,7 +351,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -398,7 +400,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/dynamic_layouts/example/ios/Runner/Info.plist b/packages/dynamic_layouts/example/ios/Runner/Info.plist index 907f329fe092..7f553465b77e 100644 --- a/packages/dynamic_layouts/example/ios/Runner/Info.plist +++ b/packages/dynamic_layouts/example/ios/Runner/Info.plist @@ -45,5 +45,7 @@ CADisableMinimumFrameDurationOnPhone + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/dynamic_layouts/example/macos/Runner.xcodeproj/project.pbxproj b/packages/dynamic_layouts/example/macos/Runner.xcodeproj/project.pbxproj index c84862c67576..d9333e4704c4 100644 --- a/packages/dynamic_layouts/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/dynamic_layouts/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -235,6 +235,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -344,7 +345,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -423,7 +424,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -470,7 +471,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/dynamic_layouts/example/pubspec.yaml b/packages/dynamic_layouts/example/pubspec.yaml index 20c5cea3cefd..51b340eea0af 100644 --- a/packages/dynamic_layouts/example/pubspec.yaml +++ b/packages/dynamic_layouts/example/pubspec.yaml @@ -6,7 +6,7 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.17.3 <4.0.0" + sdk: ">=2.18.0 <4.0.0" dependencies: cupertino_icons: ^1.0.5 diff --git a/packages/dynamic_layouts/lib/src/staggered_layout.dart b/packages/dynamic_layouts/lib/src/staggered_layout.dart index 941875a4701c..f5fb4fb86a01 100644 --- a/packages/dynamic_layouts/lib/src/staggered_layout.dart +++ b/packages/dynamic_layouts/lib/src/staggered_layout.dart @@ -47,9 +47,9 @@ class SliverGridStaggeredTileLayout extends DynamicSliverGridLayout { required this.crossAxisSpacing, required this.childCrossAxisExtent, required this.scrollDirection, - }) : assert(crossAxisCount != null && crossAxisCount > 0), - assert(crossAxisSpacing != null && crossAxisSpacing >= 0), - assert(childCrossAxisExtent != null && childCrossAxisExtent >= 0); + }) : assert(crossAxisCount > 0), + assert(crossAxisSpacing >= 0), + assert(childCrossAxisExtent >= 0); /// The number of children in the cross axis. final int crossAxisCount; @@ -200,9 +200,9 @@ class DynamicSliverGridDelegateWithFixedCrossAxisCount required super.crossAxisCount, super.mainAxisSpacing = 0.0, super.crossAxisSpacing = 0.0, - }) : assert(crossAxisCount != null && crossAxisCount > 0), - assert(mainAxisSpacing != null && mainAxisSpacing >= 0), - assert(crossAxisSpacing != null && crossAxisSpacing >= 0); + }) : assert(crossAxisCount > 0), + assert(mainAxisSpacing >= 0), + assert(crossAxisSpacing >= 0); bool _debugAssertIsValid() { assert(crossAxisCount > 0); @@ -283,9 +283,9 @@ class DynamicSliverGridDelegateWithMaxCrossAxisExtent required super.maxCrossAxisExtent, super.mainAxisSpacing = 0.0, super.crossAxisSpacing = 0.0, - }) : assert(maxCrossAxisExtent != null && maxCrossAxisExtent > 0), - assert(mainAxisSpacing != null && mainAxisSpacing >= 0), - assert(crossAxisSpacing != null && crossAxisSpacing >= 0); + }) : assert(maxCrossAxisExtent > 0), + assert(mainAxisSpacing >= 0), + assert(crossAxisSpacing >= 0); bool _debugAssertIsValid(double crossAxisExtent) { assert(crossAxisExtent > 0.0); diff --git a/packages/dynamic_layouts/lib/src/wrap_layout.dart b/packages/dynamic_layouts/lib/src/wrap_layout.dart index 83b630d5e9c4..b39ab87fa05c 100644 --- a/packages/dynamic_layouts/lib/src/wrap_layout.dart +++ b/packages/dynamic_layouts/lib/src/wrap_layout.dart @@ -54,14 +54,13 @@ class SliverGridWrappingTileLayout extends DynamicSliverGridLayout { required this.childCrossAxisExtent, required this.crossAxisExtent, required this.scrollDirection, - }) : assert(mainAxisSpacing != null && mainAxisSpacing >= 0), - assert(crossAxisSpacing != null && crossAxisSpacing >= 0), - assert(childMainAxisExtent != null && childMainAxisExtent >= 0), - assert(childCrossAxisExtent != null && childCrossAxisExtent >= 0), - assert(crossAxisExtent != null && crossAxisExtent >= 0), - assert(scrollDirection != null && - (scrollDirection == Axis.horizontal || - scrollDirection == Axis.vertical)); + }) : assert(mainAxisSpacing >= 0), + assert(crossAxisSpacing >= 0), + assert(childMainAxisExtent >= 0), + assert(childCrossAxisExtent >= 0), + assert(crossAxisExtent >= 0), + assert(scrollDirection == Axis.horizontal || + scrollDirection == Axis.vertical); /// The direction in which the layout should be built. final Axis scrollDirection; @@ -216,8 +215,8 @@ class SliverGridDelegateWithWrapping extends SliverGridDelegate { this.crossAxisSpacing = 0.0, this.childCrossAxisExtent = double.infinity, this.childMainAxisExtent = double.infinity, - }) : assert(mainAxisSpacing != null && mainAxisSpacing >= 0), - assert(crossAxisSpacing != null && crossAxisSpacing >= 0); + }) : assert(mainAxisSpacing >= 0), + assert(crossAxisSpacing >= 0); /// The number of pixels from the leading edge of one tile to the trailing /// edge of the same tile in the main axis. diff --git a/packages/dynamic_layouts/pubspec.yaml b/packages/dynamic_layouts/pubspec.yaml index 8772b265942f..9803a7fc59bb 100644 --- a/packages/dynamic_layouts/pubspec.yaml +++ b/packages/dynamic_layouts/pubspec.yaml @@ -1,14 +1,14 @@ name: dynamic_layouts description: Widgets for building dynamic grid layouts. -version: 0.0.1 +version: 0.0.1+1 issue_tracker: https://github.com/flutter/flutter/labels/p%3A%20dynamic_layouts repository: https://github.com/flutter/packages/tree/main/packages/dynamic_layouts # Temporarily unpublished while in process of releasing full package in multiple stages. publish_to: none environment: - sdk: ">=2.17.6 <4.0.0" - flutter: ">=3.0.5" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md index af45fb87c88e..a18f5ca8a486 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.3.0+5 + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. +* Bumps okhttp version to 4.11.0. + ## 0.3.0+4 * Fixes compatibility with AGP versions older than 4.2. diff --git a/packages/espresso/android/build.gradle b/packages/espresso/android/build.gradle index 1d28a98c477b..c9c0e225fd1b 100644 --- a/packages/espresso/android/build.gradle +++ b/packages/espresso/android/build.gradle @@ -61,7 +61,7 @@ android { dependencies { implementation 'com.google.guava:guava:31.1-android' - implementation 'com.squareup.okhttp3:okhttp:4.10.0' + implementation 'com.squareup.okhttp3:okhttp:4.11.0' implementation 'com.google.code.gson:gson:2.10.1' androidTestImplementation 'org.hamcrest:hamcrest:2.2' diff --git a/packages/espresso/example/.pluginToolsConfig.yaml b/packages/espresso/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/espresso/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/espresso/example/pubspec.yaml b/packages/espresso/example/pubspec.yaml index b192a804de65..70407a49f5ed 100644 --- a/packages/espresso/example/pubspec.yaml +++ b/packages/espresso/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the espresso plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/espresso/pubspec.yaml b/packages/espresso/pubspec.yaml index 0dffbdb5a1e9..011ef4f66fcd 100644 --- a/packages/espresso/pubspec.yaml +++ b/packages/espresso/pubspec.yaml @@ -3,11 +3,11 @@ 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+4 +version: 0.3.0+5 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md b/packages/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md index a28cc66839e7..f396a1ef67a0 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md +++ b/packages/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. * Aligns Dart and Flutter SDK constraints. ## 2.0.9 diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/.pluginToolsConfig.yaml b/packages/extension_google_sign_in_as_googleapis_auth/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/AppFrameworkInfo.plist b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/AppFrameworkInfo.plist index 8d4492f977ad..9625e105df39 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.pbxproj b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.pbxproj index e34694812308..351a79b106ca 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,12 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + 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 */; }; + 3FE07DF92700035B2B89559A /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 55916FA543087998FC65DEA4 /* 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 */; }; @@ -33,9 +34,12 @@ 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 = ""; }; + 55916FA543087998FC65DEA4 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6F36D2D4BD58A793981ABE04 /* 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 = ""; }; 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 = ""; }; + 807BF5889F65A1C1BEF7302C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -44,6 +48,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E6399F4B789B6B3E1623D022 /* 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 */ @@ -51,12 +56,32 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3FE07DF92700035B2B89559A /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3A6CF7EF6C0C80E66C0BA0F7 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 55916FA543087998FC65DEA4 /* libPods-Runner.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 6CA61DD4EAA7A6CE52EBE5F0 /* Pods */ = { + isa = PBXGroup; + children = ( + 6F36D2D4BD58A793981ABE04 /* Pods-Runner.debug.xcconfig */, + E6399F4B789B6B3E1623D022 /* Pods-Runner.release.xcconfig */, + 807BF5889F65A1C1BEF7302C /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -74,6 +99,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 6CA61DD4EAA7A6CE52EBE5F0 /* Pods */, + 3A6CF7EF6C0C80E66C0BA0F7 /* Frameworks */, ); sourceTree = ""; }; @@ -116,12 +143,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 26281EA1ADF7ECDE58A9CEB7 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 8EC4EC08F4C005765E6C7516 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -179,8 +208,31 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 26281EA1ADF7ECDE58A9CEB7 /* [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; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -193,8 +245,26 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 8EC4EC08F4C005765E6C7516 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -283,7 +353,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -357,7 +427,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -406,7 +476,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..21a3cc14c74e 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist index 39351b131d8c..aa6d84f63af1 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist @@ -41,5 +41,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml b/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml index 7ddbaa1be22b..f75884ca599b 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Example of Google Sign-In plugin and googleapis. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: extension_google_sign_in_as_googleapis_auth: diff --git a/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml b/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml index b48adf541fee..05cd7c58a999 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml +++ b/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml @@ -11,8 +11,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.0.9 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/file_selector/file_selector/CHANGELOG.md b/packages/file_selector/file_selector/CHANGELOG.md index febebfdcdca7..98d02784d7c8 100644 --- a/packages/file_selector/file_selector/CHANGELOG.md +++ b/packages/file_selector/file_selector/CHANGELOG.md @@ -1,5 +1,15 @@ ## NEXT +* Updates minimum supported macOS version to 10.14. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 0.9.3 + +* Adds `getDirectoryPaths` for selecting multiple directories. + +## 0.9.2+5 + +* Updates references to the deprecated `macUTIs`. * Aligns Dart and Flutter SDK constraints. ## 0.9.2+4 diff --git a/packages/file_selector/file_selector/README.md b/packages/file_selector/file_selector/README.md index d5878e2d268b..860be7fbe9d8 100644 --- a/packages/file_selector/file_selector/README.md +++ b/packages/file_selector/file_selector/README.md @@ -8,7 +8,7 @@ A Flutter plugin that manages files and interactions with file dialogs. | | iOS | Linux | macOS | Web | Windows | |-------------|---------|-------|--------|-----|-------------| -| **Support** | iOS 11+ | Any | 10.11+ | Any | Windows 10+ | +| **Support** | iOS 11+ | Any | 10.14+ | Any | Windows 10+ | ## Usage @@ -99,12 +99,12 @@ Different platforms support different type group filter options. To avoid filters that cover all platforms you are targeting, or that you conditionally pass different `XTypeGroup`s based on `Platform`. -| | Linux | macOS | Web | Windows | -|----------------|-------|--------|-----|-------------| -| `extensions` | ✔️ | ✔️ | ✔️ | ✔️ | -| `mimeTypes` | ✔️ | ✔️† | ✔️ | | -| `macUTIs` | | ✔️ | | | -| `webWildCards` | | | ✔️ | | +| | iOS | Linux | macOS | Web | Windows | +|--------------------------|-----|-------|--------|-----|-------------| +| `extensions` | | ✔️ | ✔️ | ✔️ | ✔️ | +| `mimeTypes` | | ✔️ | ✔️† | ✔️ | | +| `uniformTypeIdentifiers` | ✔️ | | ✔️ | | | +| `webWildCards` | | | | ✔️ | | † `mimeTypes` are not supported on version of macOS earlier than 11 (Big Sur). @@ -118,4 +118,4 @@ pass different `XTypeGroup`s based on `Platform`. | Choose a directory | Pick a folder and get its path | ❌ | ✔️ | ✔️ | ✔️ | ❌ | [example]:./example -[entitlement]: https://docs.flutter.dev/desktop#entitlements-and-the-app-sandbox \ No newline at end of file +[entitlement]: https://docs.flutter.dev/desktop#entitlements-and-the-app-sandbox diff --git a/packages/file_selector/file_selector/example/lib/get_multiple_directories_page.dart b/packages/file_selector/file_selector/example/lib/get_multiple_directories_page.dart new file mode 100644 index 000000000000..bdae92fd6fa8 --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/get_multiple_directories_page.dart @@ -0,0 +1,89 @@ +// 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/file_selector.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 { + /// Returns a new instance of the page. + const GetMultipleDirectoriesPage({super.key}); + + Future _getDirectoryPaths(BuildContext context) async { + const String confirmButtonText = 'Choose'; + final List directoryPaths = await getDirectoryPaths( + confirmButtonText: confirmButtonText, + ); + if (directoryPaths.isEmpty) { + // Operation was canceled by the user. + return; + } + String paths = ''; + for (final String? path in directoryPaths) { + paths += '${path!} \n'; + } + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(paths), + ); + } + } + + @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( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // 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.directoriesPaths, {super.key}); + + /// The path selected in the dialog. + final String directoriesPaths; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Selected Directories'), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(directoriesPaths), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector/example/lib/home_page.dart b/packages/file_selector/file_selector/example/lib/home_page.dart index a532dc84aab5..052dba043339 100644 --- a/packages/file_selector/file_selector/example/lib/home_page.dart +++ b/packages/file_selector/file_selector/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 multi directories dialog'), + onPressed: () => + Navigator.pushNamed(context, '/multi-directories'), + ), ], ), ), diff --git a/packages/file_selector/file_selector/example/lib/main.dart b/packages/file_selector/file_selector/example/lib/main.dart index 27b34e86fe16..19ef8c0242f8 100644 --- a/packages/file_selector/file_selector/example/lib/main.dart +++ b/packages/file_selector/file_selector/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) => GetDirectoryPage(), + '/multi-directories': (BuildContext context) => + const GetMultipleDirectoriesPage() }, ); } diff --git a/packages/file_selector/file_selector/example/macos/Podfile b/packages/file_selector/file_selector/example/macos/Podfile index dade8dfad0dc..049abe295427 100644 --- a/packages/file_selector/file_selector/example/macos/Podfile +++ b/packages/file_selector/file_selector/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/file_selector/file_selector/example/macos/Runner.xcodeproj/project.pbxproj b/packages/file_selector/file_selector/example/macos/Runner.xcodeproj/project.pbxproj index c450a1d06cf5..9f7804fdc781 100644 --- a/packages/file_selector/file_selector/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/file_selector/file_selector/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -273,6 +273,7 @@ }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -404,7 +405,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -483,7 +484,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -530,7 +531,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/file_selector/file_selector/example/pubspec.yaml b/packages/file_selector/file_selector/example/pubspec.yaml index fc9852ae6147..f4de9cbc743f 100644 --- a/packages/file_selector/file_selector/example/pubspec.yaml +++ b/packages/file_selector/file_selector/example/pubspec.yaml @@ -5,8 +5,8 @@ publish_to: none version: 1.0.0+1 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: file_selector: diff --git a/packages/file_selector/file_selector/lib/file_selector.dart b/packages/file_selector/file_selector/lib/file_selector.dart index f357af07321a..c2249565c9e8 100644 --- a/packages/file_selector/file_selector/lib/file_selector.dart +++ b/packages/file_selector/file_selector/lib/file_selector.dart @@ -106,6 +106,7 @@ Future getSavePath({ } /// Opens a directory selection dialog and returns the path chosen by the user. +/// /// This always returns `null` on the web. /// /// [initialDirectory] is the full path to the directory that will be displayed @@ -123,3 +124,24 @@ Future getDirectoryPath({ return FileSelectorPlatform.instance.getDirectoryPath( initialDirectory: initialDirectory, confirmButtonText: confirmButtonText); } + +/// Opens a directory selection dialog and returns a list of the paths chosen +/// by the user. +/// +/// This always returns an empty array on the web. +/// +/// [initialDirectory] is the full path to the directory that will be displayed +/// when the dialog is opened. When not provided, the platform will pick an +/// initial location. +/// +/// [confirmButtonText] is the text in the confirmation button of the dialog. +/// When not provided, the default OS label is used (for example, "Open"). +/// +/// Returns an empty array if the user cancels the operation. +Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, +}) async { + return FileSelectorPlatform.instance.getDirectoryPaths( + initialDirectory: initialDirectory, confirmButtonText: confirmButtonText); +} diff --git a/packages/file_selector/file_selector/pubspec.yaml b/packages/file_selector/file_selector/pubspec.yaml index 5a8e910911fa..00f11e1af886 100644 --- a/packages/file_selector/file_selector/pubspec.yaml +++ b/packages/file_selector/file_selector/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for opening and saving files, or selecting directories, using native file selection UI. repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.2+4 +version: 0.9.3 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: @@ -25,11 +25,11 @@ flutter: dependencies: file_selector_ios: ^0.5.0 - file_selector_linux: ^0.9.0 - file_selector_macos: ^0.9.0 - file_selector_platform_interface: ^2.2.0 + file_selector_linux: ^0.9.1 + file_selector_macos: ^0.9.1 + file_selector_platform_interface: ^2.3.0 file_selector_web: ^0.9.0 - file_selector_windows: ^0.9.0 + file_selector_windows: ^0.9.2 flutter: sdk: flutter diff --git a/packages/file_selector/file_selector/test/file_selector_test.dart b/packages/file_selector/file_selector/test/file_selector_test.dart index 13c986b09922..cdcebe078280 100644 --- a/packages/file_selector/file_selector/test/file_selector_test.dart +++ b/packages/file_selector/file_selector/test/file_selector_test.dart @@ -154,7 +154,7 @@ void main() { confirmButtonText: confirmButtonText, acceptedTypeGroups: acceptedTypeGroups, suggestedName: suggestedName) - ..setPathResponse(expectedSavePath); + ..setPathsResponse([expectedSavePath]); final String? savePath = await getSavePath( initialDirectory: initialDirectory, @@ -167,7 +167,7 @@ void main() { }); test('works with no arguments', () async { - fakePlatformImplementation.setPathResponse(expectedSavePath); + fakePlatformImplementation.setPathsResponse([expectedSavePath]); final String? savePath = await getSavePath(); expect(savePath, expectedSavePath); @@ -176,7 +176,7 @@ void main() { test('sets the initial directory', () async { fakePlatformImplementation ..setExpectations(initialDirectory: initialDirectory) - ..setPathResponse(expectedSavePath); + ..setPathsResponse([expectedSavePath]); final String? savePath = await getSavePath(initialDirectory: initialDirectory); @@ -186,7 +186,7 @@ void main() { test('sets the button confirmation label', () async { fakePlatformImplementation ..setExpectations(confirmButtonText: confirmButtonText) - ..setPathResponse(expectedSavePath); + ..setPathsResponse([expectedSavePath]); final String? savePath = await getSavePath(confirmButtonText: confirmButtonText); @@ -196,7 +196,7 @@ void main() { test('sets the accepted type groups', () async { fakePlatformImplementation ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) - ..setPathResponse(expectedSavePath); + ..setPathsResponse([expectedSavePath]); final String? savePath = await getSavePath(acceptedTypeGroups: acceptedTypeGroups); @@ -206,7 +206,7 @@ void main() { test('sets the suggested name', () async { fakePlatformImplementation ..setExpectations(suggestedName: suggestedName) - ..setPathResponse(expectedSavePath); + ..setPathsResponse([expectedSavePath]); final String? savePath = await getSavePath(suggestedName: suggestedName); expect(savePath, expectedSavePath); @@ -221,7 +221,7 @@ void main() { ..setExpectations( initialDirectory: initialDirectory, confirmButtonText: confirmButtonText) - ..setPathResponse(expectedDirectoryPath); + ..setPathsResponse([expectedDirectoryPath]); final String? directoryPath = await getDirectoryPath( initialDirectory: initialDirectory, @@ -232,7 +232,8 @@ void main() { }); test('works with no arguments', () async { - fakePlatformImplementation.setPathResponse(expectedDirectoryPath); + fakePlatformImplementation + .setPathsResponse([expectedDirectoryPath]); final String? directoryPath = await getDirectoryPath(); expect(directoryPath, expectedDirectoryPath); @@ -241,7 +242,7 @@ void main() { test('sets the initial directory', () async { fakePlatformImplementation ..setExpectations(initialDirectory: initialDirectory) - ..setPathResponse(expectedDirectoryPath); + ..setPathsResponse([expectedDirectoryPath]); final String? directoryPath = await getDirectoryPath(initialDirectory: initialDirectory); @@ -251,13 +252,62 @@ void main() { test('sets the button confirmation label', () async { fakePlatformImplementation ..setExpectations(confirmButtonText: confirmButtonText) - ..setPathResponse(expectedDirectoryPath); + ..setPathsResponse([expectedDirectoryPath]); final String? directoryPath = await getDirectoryPath(confirmButtonText: confirmButtonText); expect(directoryPath, expectedDirectoryPath); }); }); + + group('getDirectoryPaths', () { + const List expectedDirectoryPaths = [ + '/example/path', + '/example/2/path' + ]; + + test('works', () async { + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText) + ..setPathsResponse(expectedDirectoryPaths); + + final List directoryPaths = await getDirectoryPaths( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + ); + + expect(directoryPaths, expectedDirectoryPaths); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setPathsResponse(expectedDirectoryPaths); + + final List directoryPaths = await getDirectoryPaths(); + expect(directoryPaths, expectedDirectoryPaths); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setPathsResponse(expectedDirectoryPaths); + + final List directoryPaths = + await getDirectoryPaths(initialDirectory: initialDirectory); + expect(directoryPaths, expectedDirectoryPaths); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setPathsResponse(expectedDirectoryPaths); + + final List directoryPaths = + await getDirectoryPaths(confirmButtonText: confirmButtonText); + expect(directoryPaths, expectedDirectoryPaths); + }); + }); } class FakeFileSelector extends Fake @@ -270,7 +320,7 @@ class FakeFileSelector extends Fake String? suggestedName; // Return values. List? files; - String? path; + List? paths; void setExpectations({ List acceptedTypeGroups = const [], @@ -290,8 +340,8 @@ class FakeFileSelector extends Fake } // ignore: use_setters_to_change_properties - void setPathResponse(String path) { - this.path = path; + void setPathsResponse(List paths) { + this.paths = paths; } @override @@ -329,7 +379,7 @@ class FakeFileSelector extends Fake expect(initialDirectory, this.initialDirectory); expect(suggestedName, this.suggestedName); expect(confirmButtonText, this.confirmButtonText); - return path; + return paths?[0]; } @override @@ -339,6 +389,16 @@ class FakeFileSelector extends Fake }) async { expect(initialDirectory, this.initialDirectory); expect(confirmButtonText, this.confirmButtonText); - return path; + return paths?[0]; + } + + @override + Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, + }) async { + expect(initialDirectory, this.initialDirectory); + expect(confirmButtonText, this.confirmButtonText); + return paths!; } } diff --git a/packages/file_selector/file_selector_ios/CHANGELOG.md b/packages/file_selector/file_selector_ios/CHANGELOG.md index 568b6ceee97e..8a48d5b089db 100644 --- a/packages/file_selector/file_selector_ios/CHANGELOG.md +++ b/packages/file_selector/file_selector_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.1+4 + +* Updates references to the deprecated `macUTIs`. + ## 0.5.1+3 * Updates pigeon to fix warnings with clang 15. diff --git a/packages/file_selector/file_selector_ios/example/lib/open_image_page.dart b/packages/file_selector/file_selector_ios/example/lib/open_image_page.dart index a89c1bf1c082..e4fd5dd07650 100644 --- a/packages/file_selector/file_selector_ios/example/lib/open_image_page.dart +++ b/packages/file_selector/file_selector_ios/example/lib/open_image_page.dart @@ -18,7 +18,7 @@ class OpenImagePage extends StatelessWidget { const XTypeGroup typeGroup = XTypeGroup( label: 'images', extensions: ['jpg', 'png'], - macUTIs: ['public.image'], + uniformTypeIdentifiers: ['public.image'], ); final XFile? file = await FileSelectorPlatform.instance .openFile(acceptedTypeGroups: [typeGroup]); diff --git a/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.dart index a101a45ddfc3..cdbf5f2f21ee 100644 --- a/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.dart +++ b/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.dart @@ -18,12 +18,12 @@ class OpenMultipleImagesPage extends StatelessWidget { const XTypeGroup jpgsTypeGroup = XTypeGroup( label: 'JPEGs', extensions: ['jpg', 'jpeg'], - macUTIs: ['public.jpeg'], + uniformTypeIdentifiers: ['public.jpeg'], ); const XTypeGroup pngTypeGroup = XTypeGroup( label: 'PNGs', extensions: ['png'], - macUTIs: ['public.png'], + uniformTypeIdentifiers: ['public.png'], ); final List files = await FileSelectorPlatform.instance .openFiles(acceptedTypeGroups: [ diff --git a/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart b/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart index 14ec0ea9b890..f606fdc7f9f8 100644 --- a/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart +++ b/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart @@ -15,7 +15,7 @@ class OpenTextPage extends StatelessWidget { const XTypeGroup typeGroup = XTypeGroup( label: 'text', extensions: ['txt', 'json'], - macUTIs: ['public.text'], + uniformTypeIdentifiers: ['public.text'], ); final XFile? file = await FileSelectorPlatform.instance .openFile(acceptedTypeGroups: [typeGroup]); diff --git a/packages/file_selector/file_selector_ios/lib/file_selector_ios.dart b/packages/file_selector/file_selector_ios/lib/file_selector_ios.dart index e75f67e4f1bd..22349b6232ec 100644 --- a/packages/file_selector/file_selector_ios/lib/file_selector_ios.dart +++ b/packages/file_selector/file_selector_ios/lib/file_selector_ios.dart @@ -53,11 +53,11 @@ class FileSelectorIOS extends FileSelectorPlatform { if (typeGroup.allowsAny) { return []; } - if (typeGroup.macUTIs?.isEmpty ?? true) { + if (typeGroup.uniformTypeIdentifiers?.isEmpty ?? true) { throw ArgumentError('The provided type group $typeGroup should either ' - 'allow all files, or have a non-empty "macUTIs"'); + 'allow all files, or have a non-empty "uniformTypeIdentifiers"'); } - allowedUTIs.addAll(typeGroup.macUTIs!); + allowedUTIs.addAll(typeGroup.uniformTypeIdentifiers!); } return allowedUTIs; } diff --git a/packages/file_selector/file_selector_ios/pubspec.yaml b/packages/file_selector/file_selector_ios/pubspec.yaml index aa94300b0b94..944f46709fd2 100644 --- a/packages/file_selector/file_selector_ios/pubspec.yaml +++ b/packages/file_selector/file_selector_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_ios description: iOS implementation of the file_selector plugin. repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.5.1+3 +version: 0.5.1+4 environment: sdk: ">=2.18.0 <4.0.0" @@ -17,7 +17,7 @@ flutter: pluginClass: FFSFileSelectorPlugin dependencies: - file_selector_platform_interface: ^2.2.0 + file_selector_platform_interface: ^2.3.0 flutter: sdk: flutter diff --git a/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart b/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart index e10ad17a2fb4..6d3c3c684cac 100644 --- a/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart +++ b/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart @@ -40,14 +40,14 @@ void main() { label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], - macUTIs: ['public.text'], + uniformTypeIdentifiers: ['public.text'], ); const XTypeGroup groupTwo = XTypeGroup( label: 'image', extensions: ['jpg'], mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], + uniformTypeIdentifiers: ['public.image'], webWildCards: ['image/*']); await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); @@ -56,7 +56,7 @@ void main() { final FileSelectorConfig config = result.captured[0] as FileSelectorConfig; - // iOS only accepts macUTIs. + // iOS only accepts uniformTypeIdentifiers. expect(listEquals(config.utis, ['public.text', 'public.image']), isTrue); expect(config.allowMultiSelection, isFalse); @@ -92,14 +92,14 @@ void main() { label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], - macUTIs: ['public.text'], + uniformTypeIdentifiers: ['public.text'], ); const XTypeGroup groupTwo = XTypeGroup( label: 'image', extensions: ['jpg'], mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], + uniformTypeIdentifiers: ['public.image'], webWildCards: ['image/*']); await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); @@ -108,7 +108,7 @@ void main() { final FileSelectorConfig config = result.captured[0] as FileSelectorConfig; - // iOS only accepts macUTIs. + // iOS only accepts uniformTypeIdentifiers. expect(listEquals(config.utis, ['public.text', 'public.image']), isTrue); expect(config.allowMultiSelection, isTrue); diff --git a/packages/file_selector/file_selector_linux/CHANGELOG.md b/packages/file_selector/file_selector_linux/CHANGELOG.md index 262c418f5ddf..88886301d3a1 100644 --- a/packages/file_selector/file_selector_linux/CHANGELOG.md +++ b/packages/file_selector/file_selector_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 0.9.1+3 * Sets a cmake_policy compatibility version to fix build warnings. diff --git a/packages/file_selector/file_selector_linux/example/pubspec.yaml b/packages/file_selector/file_selector_linux/example/pubspec.yaml index 84362a684d93..1596eb3f769f 100644 --- a/packages/file_selector/file_selector_linux/example/pubspec.yaml +++ b/packages/file_selector/file_selector_linux/example/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: file_selector_linux: diff --git a/packages/file_selector/file_selector_linux/pubspec.yaml b/packages/file_selector/file_selector_linux/pubspec.yaml index 8064e68d3587..238e0aef741b 100644 --- a/packages/file_selector/file_selector_linux/pubspec.yaml +++ b/packages/file_selector/file_selector_linux/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 0.9.1+3 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart index 53a549da3d4a..5127d28b7f67 100644 --- a/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart +++ b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart @@ -38,15 +38,12 @@ void main() { label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], - macUTIs: ['public.text'], ); const XTypeGroup groupTwo = XTypeGroup( label: 'image', extensions: ['jpg'], mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], - webWildCards: ['image/*'], ); await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); @@ -144,15 +141,12 @@ void main() { label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], - macUTIs: ['public.text'], ); const XTypeGroup groupTwo = XTypeGroup( label: 'image', extensions: ['jpg'], mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], - webWildCards: ['image/*'], ); await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); @@ -250,15 +244,12 @@ void main() { label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], - macUTIs: ['public.text'], ); const XTypeGroup groupTwo = XTypeGroup( label: 'image', extensions: ['jpg'], mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], - webWildCards: ['image/*'], ); await plugin diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md index a83f52552fdf..e697974a390f 100644 --- a/packages/file_selector/file_selector_macos/CHANGELOG.md +++ b/packages/file_selector/file_selector_macos/CHANGELOG.md @@ -1,3 +1,19 @@ +## NEXT + +* Updates minimum supported macOS version to 10.14. + +## 0.9.2 + +* Adds support for MIME types on macOS 11+. + +## 0.9.1+1 + +* Updates references to the deprecated `macUTIs`. + +## 0.9.1 + +* Adds `getDirectoryPaths` implementation. + ## 0.9.0+8 * Updates pigeon for null value handling fixes. diff --git a/packages/file_selector/file_selector_macos/example/lib/get_multiple_directories_page.dart b/packages/file_selector/file_selector_macos/example/lib/get_multiple_directories_page.dart new file mode 100644 index 000000000000..c5fcecec7c77 --- /dev/null +++ b/packages/file_selector/file_selector_macos/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_macos/example/lib/home_page.dart b/packages/file_selector/file_selector_macos/example/lib/home_page.dart index 366ff5144245..3c80f4405785 100644 --- a/packages/file_selector/file_selector_macos/example/lib/home_page.dart +++ b/packages/file_selector/file_selector_macos/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_macos/example/lib/main.dart b/packages/file_selector/file_selector_macos/example/lib/main.dart index bfd2c2fdbfc1..a88f850f5d6d 100644 --- a/packages/file_selector/file_selector_macos/example/lib/main.dart +++ b/packages/file_selector/file_selector_macos/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_macos/example/macos/Podfile b/packages/file_selector/file_selector_macos/example/macos/Podfile index dade8dfad0dc..049abe295427 100644 --- a/packages/file_selector/file_selector_macos/example/macos/Podfile +++ b/packages/file_selector/file_selector_macos/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/project.pbxproj index fa8d272d4ee0..7d276571a8ec 100644 --- a/packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -314,6 +314,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -475,7 +476,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -608,7 +609,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -655,7 +656,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift index fcd39c61117c..a77f4217311b 100644 --- a/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift +++ b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift @@ -3,6 +3,7 @@ // found in the LICENSE file. import FlutterMacOS +import UniformTypeIdentifiers import XCTest @testable import file_selector_macos @@ -160,7 +161,7 @@ class exampleTests: XCTestCase { baseOptions: SavePanelOptions( allowedFileTypes: AllowedTypes( extensions: ["txt", "json"], - mimeTypes: [], + mimeTypes: ["text/html"], utis: ["public.text", "public.image"]))) plugin.displayOpenPanel(options: options) { result in switch result { @@ -175,7 +176,62 @@ class exampleTests: XCTestCase { wait(for: [called], timeout: 0.5) XCTAssertNotNil(panelController.openPanel) if let panel = panelController.openPanel { + if #available(macOS 11.0, *) { + XCTAssertTrue(panel.allowedContentTypes.contains(UTType.plainText)) + XCTAssertTrue(panel.allowedContentTypes.contains(UTType.json)) + XCTAssertTrue(panel.allowedContentTypes.contains(UTType.html)) + XCTAssertTrue(panel.allowedContentTypes.contains(UTType.image)) + } else { + // MIME type is not supported for the legacy codepath, but the rest should be set. + XCTAssertEqual(panel.allowedFileTypes, ["txt", "json", "public.text", "public.image"]) + } + } + } + + func testOpenWithFilterLegacy() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + plugin.forceLegacyTypes = true + + let returnPath = "/foo/bar" + panelController.openURLs = [URL(fileURLWithPath: returnPath)] + + let called = XCTestExpectation() + let options = OpenPanelOptions( + allowsMultipleSelection: true, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions( + allowedFileTypes: AllowedTypes( + extensions: ["txt", "json"], + mimeTypes: ["text/html"], + utis: ["public.text", "public.image"]))) + plugin.displayOpenPanel(options: options) { result in + switch result { + case .success(let paths): + XCTAssertEqual(paths[0], returnPath) + case .failure(let error): + XCTFail("\(error)") + } + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.openPanel) + if let panel = panelController.openPanel { + // On the legacy path, the allowedFileTypes should be set directly. XCTAssertEqual(panel.allowedFileTypes, ["txt", "json", "public.text", "public.image"]) + + // They should also be translated to corresponding allowed content types. + if #available(macOS 11.0, *) { + XCTAssertTrue(panel.allowedContentTypes.contains(UTType.plainText)) + XCTAssertTrue(panel.allowedContentTypes.contains(UTType.json)) + XCTAssertTrue(panel.allowedContentTypes.contains(UTType.image)) + // MIME type is not supported for the legacy codepath. + XCTAssertFalse(panel.allowedContentTypes.contains(UTType.html)) + } } } @@ -346,4 +402,64 @@ class exampleTests: XCTestCase { XCTAssertNotNil(panelController.openPanel) } + func testGetDirectoriesMultiple() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let returnPaths = ["/foo/bar", "/foo/test"]; + panelController.openURLs = returnPaths.map({ path in URL(fileURLWithPath: path) }) + + let called = XCTestExpectation() + let options = OpenPanelOptions( + allowsMultipleSelection: true, + canChooseDirectories: true, + canChooseFiles: false, + baseOptions: SavePanelOptions()) + plugin.displayOpenPanel(options: options) { result in + switch result { + case .success(let paths): + XCTAssertEqual(paths, returnPaths) + case .failure(let error): + XCTFail("\(error)") + } + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.openPanel) + if let panel = panelController.openPanel { + XCTAssertTrue(panel.canChooseDirectories) + // For consistency across platforms, file selection is disabled. + XCTAssertFalse(panel.canChooseFiles) + XCTAssertTrue(panel.allowsMultipleSelection) + } + } + + func testGetDirectoryMultipleCancel() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let called = XCTestExpectation() + let options = OpenPanelOptions( + allowsMultipleSelection: true, + canChooseDirectories: true, + canChooseFiles: false, + baseOptions: SavePanelOptions()) + plugin.displayOpenPanel(options: options) { result in + switch result { + case .success(let paths): + XCTAssertEqual(paths.count, 0) + case .failure(let error): + XCTFail("\(error)") + } + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.openPanel) + } } diff --git a/packages/file_selector/file_selector_macos/example/pubspec.yaml b/packages/file_selector/file_selector_macos/example/pubspec.yaml index b8e38bd4e8ab..9e2d831f75cf 100644 --- a/packages/file_selector/file_selector_macos/example/pubspec.yaml +++ b/packages/file_selector/file_selector_macos/example/pubspec.yaml @@ -15,7 +15,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: .. - 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_macos/lib/file_selector_macos.dart b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart index f8a087fa6877..293c1e20e773 100644 --- a/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart +++ b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart @@ -85,6 +85,23 @@ class FileSelectorMacOS extends FileSelectorPlatform { return paths.isEmpty ? null : paths.first; } + @override + Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, + }) async { + final List paths = + await _hostApi.displayOpenPanel(OpenPanelOptions( + allowsMultipleSelection: true, + canChooseDirectories: true, + canChooseFiles: false, + baseOptions: SavePanelOptions( + directoryPath: initialDirectory, + prompt: confirmButtonText, + ))); + return paths.isEmpty ? [] : List.from(paths); + } + // Converts the type group list into a flat list of all allowed types, since // macOS doesn't support filter groups. AllowedTypes? _allowedTypesFromTypeGroups(List? typeGroups) { @@ -104,17 +121,17 @@ class FileSelectorMacOS extends FileSelectorPlatform { // Reject a filter that isn't an allow-any, but doesn't set any // macOS-supported filter categories. if ((typeGroup.extensions?.isEmpty ?? true) && - (typeGroup.macUTIs?.isEmpty ?? true) && + (typeGroup.uniformTypeIdentifiers?.isEmpty ?? true) && (typeGroup.mimeTypes?.isEmpty ?? true)) { throw ArgumentError('Provided type group $typeGroup does not allow ' 'all files, but does not set any of the macOS-supported filter ' - 'categories. At least one of "extensions", "macUTIs", or ' - '"mimeTypes" must be non-empty for macOS if anything is ' - 'non-empty.'); + 'categories. At least one of "extensions", ' + '"uniformTypeIdentifiers", or "mimeTypes" must be non-empty for ' + 'macOS if anything is non-empty.'); } allowedTypes.extensions.addAll(typeGroup.extensions ?? []); allowedTypes.mimeTypes.addAll(typeGroup.mimeTypes ?? []); - allowedTypes.utis.addAll(typeGroup.macUTIs ?? []); + allowedTypes.utis.addAll(typeGroup.uniformTypeIdentifiers ?? []); } return allowedTypes; diff --git a/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift b/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift index 836fcf94244b..83103325cf5c 100644 --- a/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift +++ b/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift @@ -2,8 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import Cocoa import FlutterMacOS -import Foundation +import UniformTypeIdentifiers /// Protocol for showing panels, allowing for depenedency injection in tests. protocol PanelController { @@ -48,6 +49,8 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin, FileSelectorApi { private let openDirectoryMethod = "getDirectoryPath" private let saveMethod = "getSavePath" + var forceLegacyTypes = false + public static func register(with registrar: FlutterPluginRegistrar) { let instance = FileSelectorPlugin( viewProvider: DefaultViewProvider(registrar: registrar), @@ -96,16 +99,31 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin, FileSelectorApi { } if let acceptedTypes = options.allowedFileTypes { - var allowedTypes: [String] = [] - // The array values are non-null by convention even though Pigeon can't currently express - // that via the types; see messages.dart. - allowedTypes.append(contentsOf: acceptedTypes.extensions.map({ $0! })) - allowedTypes.append(contentsOf: acceptedTypes.utis.map({ $0! })) - // TODO: Add support for mimeTypes in macOS 11+. See - // https://github.com/flutter/flutter/issues/117843 - - if !allowedTypes.isEmpty { - panel.allowedFileTypes = allowedTypes + if #available(macOS 11, *), !forceLegacyTypes { + var allowedTypes: [UTType] = [] + // The array values are non-null by convention even though Pigeon can't currently express + // that via the types; see messages.dart and https://github.com/flutter/flutter/issues/97848 + allowedTypes.append(contentsOf: acceptedTypes.utis.compactMap({ UTType($0!) })) + allowedTypes.append( + contentsOf: acceptedTypes.extensions.flatMap({ + UTType.types(tag: $0!, tagClass: UTTagClass.filenameExtension, conformingTo: nil) + })) + allowedTypes.append( + contentsOf: acceptedTypes.mimeTypes.flatMap({ + UTType.types(tag: $0!, tagClass: UTTagClass.mimeType, conformingTo: nil) + })) + if !allowedTypes.isEmpty { + panel.allowedContentTypes = allowedTypes + } + } else { + var allowedTypes: [String] = [] + // The array values are non-null by convention even though Pigeon can't currently express + // that via the types; see messages.dart and https://github.com/flutter/flutter/issues/97848 + allowedTypes.append(contentsOf: acceptedTypes.extensions.map({ $0! })) + allowedTypes.append(contentsOf: acceptedTypes.utis.map({ $0! })) + if !allowedTypes.isEmpty { + panel.allowedFileTypes = allowedTypes + } } } } diff --git a/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift b/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift index 828c499a2182..67007e100811 100644 --- a/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift +++ b/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift @@ -5,12 +5,13 @@ // See also: https://pub.dev/packages/pigeon import Foundation + #if os(iOS) -import Flutter + import Flutter #elseif os(macOS) -import FlutterMacOS + import FlutterMacOS #else -#error("Unsupported platform.") + #error("Unsupported platform.") #endif private func wrapResult(_ result: Any?) -> [Any?] { @@ -22,13 +23,13 @@ private func wrapError(_ error: Any) -> [Any?] { return [ flutterError.code, flutterError.message, - flutterError.details + flutterError.details, ] } return [ "\(error)", "\(type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)" + "Stacktrace: \(Thread.callStackSymbols)", ] } @@ -140,14 +141,14 @@ struct OpenPanelOptions { private class FileSelectorApiCodecReader: FlutterStandardReader { override func readValue(ofType type: UInt8) -> Any? { switch type { - case 128: - return AllowedTypes.fromList(self.readValue() as! [Any]) - case 129: - return OpenPanelOptions.fromList(self.readValue() as! [Any]) - case 130: - return SavePanelOptions.fromList(self.readValue() as! [Any]) - default: - return super.readValue(ofType: type) + case 128: + return AllowedTypes.fromList(self.readValue() as! [Any]) + case 129: + return OpenPanelOptions.fromList(self.readValue() as! [Any]) + case 130: + return SavePanelOptions.fromList(self.readValue() as! [Any]) + default: + return super.readValue(ofType: type) } } } @@ -189,11 +190,13 @@ protocol FileSelectorApi { /// selected paths. /// /// An empty list corresponds to a cancelled selection. - func displayOpenPanel(options: OpenPanelOptions, completion: @escaping (Result<[String?], Error>) -> Void) + func displayOpenPanel( + options: OpenPanelOptions, completion: @escaping (Result<[String?], Error>) -> Void) /// Shows a save panel with the given [options], returning the selected path. /// /// A null return corresponds to a cancelled save. - func displaySavePanel(options: SavePanelOptions, completion: @escaping (Result) -> Void) + func displaySavePanel( + options: SavePanelOptions, completion: @escaping (Result) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -206,17 +209,19 @@ class FileSelectorApiSetup { /// selected paths. /// /// An empty list corresponds to a cancelled selection. - let displayOpenPanelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.FileSelectorApi.displayOpenPanel", binaryMessenger: binaryMessenger, codec: codec) + let displayOpenPanelChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.FileSelectorApi.displayOpenPanel", binaryMessenger: binaryMessenger, + codec: codec) if let api = api { displayOpenPanelChannel.setMessageHandler { message, reply in let args = message as! [Any] let optionsArg = args[0] as! OpenPanelOptions api.displayOpenPanel(options: optionsArg) { result in switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) } } } @@ -226,17 +231,19 @@ class FileSelectorApiSetup { /// Shows a save panel with the given [options], returning the selected path. /// /// A null return corresponds to a cancelled save. - let displaySavePanelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.FileSelectorApi.displaySavePanel", binaryMessenger: binaryMessenger, codec: codec) + let displaySavePanelChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.FileSelectorApi.displaySavePanel", binaryMessenger: binaryMessenger, + codec: codec) if let api = api { displaySavePanelChannel.setMessageHandler { message, reply in let args = message as! [Any] let optionsArg = args[0] as! SavePanelOptions api.displaySavePanel(options: optionsArg) { result in switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) } } } diff --git a/packages/file_selector/file_selector_macos/macos/file_selector_macos.podspec b/packages/file_selector/file_selector_macos/macos/file_selector_macos.podspec index f0b537dcff58..fb460b41c97d 100644 --- a/packages/file_selector/file_selector_macos/macos/file_selector_macos.podspec +++ b/packages/file_selector/file_selector_macos/macos/file_selector_macos.podspec @@ -15,7 +15,7 @@ Displays native macOS open and save panels. s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' - s.platform = :osx, '10.11' + s.platform = :osx, '10.14' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' end diff --git a/packages/file_selector/file_selector_macos/pubspec.yaml b/packages/file_selector/file_selector_macos/pubspec.yaml index 4b922261bbe4..f55079f380a6 100644 --- a/packages/file_selector/file_selector_macos/pubspec.yaml +++ b/packages/file_selector/file_selector_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_macos description: macOS implementation of the file_selector plugin. repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.0+8 +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_macos/test/file_selector_macos_test.dart b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart index 181409e6f1b4..6450e6f3b0a7 100644 --- a/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart +++ b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart @@ -69,14 +69,14 @@ void main() { label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], - macUTIs: ['public.text'], + uniformTypeIdentifiers: ['public.text'], ); const XTypeGroup groupTwo = XTypeGroup( label: 'image', extensions: ['jpg'], mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], + uniformTypeIdentifiers: ['public.image'], webWildCards: ['image/*']); await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); @@ -165,14 +165,14 @@ void main() { label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], - macUTIs: ['public.text'], + uniformTypeIdentifiers: ['public.text'], ); const XTypeGroup groupTwo = XTypeGroup( label: 'image', extensions: ['jpg'], mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], + uniformTypeIdentifiers: ['public.image'], webWildCards: ['image/*']); await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); @@ -256,14 +256,14 @@ void main() { label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], - macUTIs: ['public.text'], + uniformTypeIdentifiers: ['public.text'], ); const XTypeGroup groupTwo = XTypeGroup( label: 'image', extensions: ['jpg'], mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], + uniformTypeIdentifiers: ['public.image'], webWildCards: ['image/*']); await plugin @@ -317,6 +317,31 @@ void main() { plugin.getSavePath(acceptedTypeGroups: [group]), completes); }); + + test('ignores all type groups if any of them is a wildcard', () async { + await plugin.getSavePath(acceptedTypeGroups: [ + const XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + uniformTypeIdentifiers: ['public.text'], + ), + const XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + uniformTypeIdentifiers: ['public.image'], + ), + const XTypeGroup( + label: 'any', + ), + ]); + + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.allowedFileTypes, null); + }); }); group('getDirectoryPath', () { @@ -366,28 +391,51 @@ void main() { }); }); - test('ignores all type groups if any of them is a wildcard', () async { - await plugin.getSavePath(acceptedTypeGroups: [ - const XTypeGroup( - label: 'text', - extensions: ['txt'], - mimeTypes: ['text/plain'], - macUTIs: ['public.text'], - ), - const XTypeGroup( - label: 'image', - extensions: ['jpg'], - mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], - ), - const XTypeGroup( - label: 'any', - ), - ]); - - final VerificationResult result = - verify(mockApi.displaySavePanel(captureAny)); - final SavePanelOptions options = result.captured[0] as SavePanelOptions; - expect(options.allowedFileTypes, null); + group('getDirectoryPaths', () { + test('works as expected with no arguments', () async { + when(mockApi.displayOpenPanel(any)).thenAnswer((_) async => + ['firstDirectory', 'secondDirectory', 'thirdDirectory']); + + final List path = await plugin.getDirectoryPaths(); + + expect(path, + ['firstDirectory', 'secondDirectory', 'thirdDirectory']); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.allowsMultipleSelection, true); + expect(options.canChooseFiles, false); + expect(options.canChooseDirectories, true); + expect(options.baseOptions.allowedFileTypes, null); + expect(options.baseOptions.directoryPath, null); + expect(options.baseOptions.nameFieldStringValue, null); + expect(options.baseOptions.prompt, null); + }); + + test('handles cancel', () async { + when(mockApi.displayOpenPanel(any)).thenAnswer((_) async => []); + + final List paths = await plugin.getDirectoryPaths(); + + expect(paths, []); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.getDirectoryPaths(confirmButtonText: 'Select directories'); + + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.prompt, 'Select directories'); + }); + + test('passes initialDirectory correctly', () async { + await plugin.getDirectoryPaths(initialDirectory: '/example/directory'); + + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.directoryPath, '/example/directory'); + }); }); } diff --git a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md index b66fa72df03d..0ede4ddd8d6d 100644 --- a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md +++ b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md @@ -1,5 +1,10 @@ ## NEXT +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 2.5.0 + +* Deprecates `macUTIs` in favor of `uniformTypeIdentifiers`. * Aligns Dart and Flutter SDK constraints. ## 2.4.1 diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart index e12b431d91d8..0a556b3a2f0f 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart @@ -14,9 +14,9 @@ class XTypeGroup { this.label, List? extensions, this.mimeTypes, - List? macUTIs, List? uniformTypeIdentifiers, this.webWildCards, + @Deprecated('Use uniformTypeIdentifiers instead') List? macUTIs, }) : _extensions = extensions, assert(uniformTypeIdentifiers == null || macUTIs == null, 'Only one of uniformTypeIdentifiers or macUTIs can be non-null'), @@ -47,8 +47,12 @@ class XTypeGroup { 'label': label, 'extensions': extensions, 'mimeTypes': mimeTypes, - 'macUTIs': macUTIs, + 'uniformTypeIdentifiers': uniformTypeIdentifiers, 'webWildCards': webWildCards, + // This is kept for backwards compatibility with anything that was + // relying on it, including implementers of `MethodChannelFileSelector` + // (since toJSON is used in the method channel parameter serialization). + 'macUTIs': uniformTypeIdentifiers, }; } @@ -56,11 +60,12 @@ class XTypeGroup { bool get allowsAny { return (extensions?.isEmpty ?? true) && (mimeTypes?.isEmpty ?? true) && - (macUTIs?.isEmpty ?? true) && + (uniformTypeIdentifiers?.isEmpty ?? true) && (webWildCards?.isEmpty ?? true); } /// Returns the list of uniform type identifiers for this group + @Deprecated('Use uniformTypeIdentifiers instead') List? get macUTIs => uniformTypeIdentifiers; static List? _removeLeadingDots(List? exts) => exts diff --git a/packages/file_selector/file_selector_platform_interface/pubspec.yaml b/packages/file_selector/file_selector_platform_interface/pubspec.yaml index a376719cf068..94023c968099 100644 --- a/packages/file_selector/file_selector_platform_interface/pubspec.yaml +++ b/packages/file_selector/file_selector_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/packages/tree/main/packages/file_selector issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%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.4.1 +version: 2.5.0 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: cross_file: ^0.3.0 diff --git a/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart b/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart index c5438f7ecbc2..2c7d455dd4d8 100644 --- a/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart +++ b/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart @@ -35,14 +35,14 @@ void main() { label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], - macUTIs: ['public.text'], + uniformTypeIdentifiers: ['public.text'], ); const XTypeGroup groupTwo = XTypeGroup( label: 'image', extensions: ['jpg'], mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], + uniformTypeIdentifiers: ['public.image'], webWildCards: ['image/*']); await plugin @@ -97,14 +97,14 @@ void main() { label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], - macUTIs: ['public.text'], + uniformTypeIdentifiers: ['public.text'], ); const XTypeGroup groupTwo = XTypeGroup( label: 'image', extensions: ['jpg'], mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], + uniformTypeIdentifiers: ['public.image'], webWildCards: ['image/*']); await plugin @@ -160,14 +160,14 @@ void main() { label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], - macUTIs: ['public.text'], + uniformTypeIdentifiers: ['public.text'], ); const XTypeGroup groupTwo = XTypeGroup( label: 'image', extensions: ['jpg'], mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], + uniformTypeIdentifiers: ['public.image'], webWildCards: ['image/*']); await plugin diff --git a/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart b/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart index 5ac5722716c7..0a36c82be9bc 100644 --- a/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart +++ b/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart @@ -10,14 +10,14 @@ void main() { test('toJSON() creates correct map', () { const List extensions = ['txt', 'jpg']; const List mimeTypes = ['text/plain']; - const List macUTIs = ['public.plain-text']; + const List uniformTypeIdentifiers = ['public.plain-text']; const List webWildCards = ['image/*']; const String label = 'test group'; const XTypeGroup group = XTypeGroup( label: label, extensions: extensions, mimeTypes: mimeTypes, - macUTIs: macUTIs, + uniformTypeIdentifiers: uniformTypeIdentifiers, webWildCards: webWildCards, ); @@ -25,8 +25,10 @@ void main() { expect(jsonMap['label'], label); expect(jsonMap['extensions'], extensions); expect(jsonMap['mimeTypes'], mimeTypes); - expect(jsonMap['macUTIs'], macUTIs); + expect(jsonMap['uniformTypeIdentifiers'], uniformTypeIdentifiers); expect(jsonMap['webWildCards'], webWildCards); + // Validate the legacy key for backwards compatibility. + expect(jsonMap['macUTIs'], uniformTypeIdentifiers); }); test('a wildcard group can be created', () { @@ -37,7 +39,7 @@ void main() { final Map jsonMap = group.toJSON(); expect(jsonMap['extensions'], null); expect(jsonMap['mimeTypes'], null); - expect(jsonMap['macUTIs'], null); + expect(jsonMap['uniformTypeIdentifiers'], null); expect(jsonMap['webWildCards'], null); expect(group.allowsAny, true); }); @@ -47,7 +49,7 @@ void main() { label: 'Any', extensions: [], mimeTypes: [], - macUTIs: [], + uniformTypeIdentifiers: [], webWildCards: [], ); @@ -59,8 +61,8 @@ void main() { XTypeGroup(label: 'extensions', extensions: ['txt']); const XTypeGroup mimeOnly = XTypeGroup(label: 'mime', mimeTypes: ['text/plain']); - const XTypeGroup utiOnly = - XTypeGroup(label: 'utis', macUTIs: ['public.text']); + const XTypeGroup utiOnly = XTypeGroup( + label: 'utis', uniformTypeIdentifiers: ['public.text']); const XTypeGroup webOnly = XTypeGroup(label: 'web', webWildCards: ['.txt']); @@ -70,67 +72,73 @@ void main() { expect(webOnly.allowsAny, false); }); - test('passing only macUTIs should fill uniformTypeIdentifiers', () { - const List macUTIs = ['public.plain-text']; - const XTypeGroup group = XTypeGroup( - macUTIs: macUTIs, - ); - - expect(group.uniformTypeIdentifiers, macUTIs); - }); - - test( - 'passing only uniformTypeIdentifiers should fill uniformTypeIdentifiers', - () { - const List uniformTypeIdentifiers = ['public.plain-text']; - const XTypeGroup group = XTypeGroup( - uniformTypeIdentifiers: uniformTypeIdentifiers, - ); - - expect(group.uniformTypeIdentifiers, uniformTypeIdentifiers); - }); - - test('macUTIs getter return macUTIs value passed in constructor', () { - const List macUTIs = ['public.plain-text']; - const XTypeGroup group = XTypeGroup( - macUTIs: macUTIs, - ); - - expect(group.macUTIs, macUTIs); - }); - - test( - 'macUTIs getter returns uniformTypeIdentifiers value passed in constructor', - () { - const List uniformTypeIdentifiers = ['public.plain-text']; - const XTypeGroup group = XTypeGroup( - uniformTypeIdentifiers: uniformTypeIdentifiers, - ); - - expect(group.macUTIs, uniformTypeIdentifiers); - }); - - test('passing both uniformTypeIdentifiers and macUTIs should throw', () { - const List macUTIs = ['public.plain-text']; - const List uniformTypeIndentifiers = [ - 'public.plain-images' - ]; - expect( - () => XTypeGroup( - macUTIs: macUTIs, - uniformTypeIdentifiers: uniformTypeIndentifiers), - throwsA(predicate((Object? e) => - e is AssertionError && - e.message == - 'Only one of uniformTypeIdentifiers or macUTIs can be non-null'))); - }); - - test( - 'having uniformTypeIdentifiers and macUTIs as null should leave uniformTypeIdentifiers as null', - () { - const XTypeGroup group = XTypeGroup(); - - expect(group.uniformTypeIdentifiers, null); + group('macUTIs -> uniformTypeIdentifiers transition', () { + test('passing only macUTIs should fill uniformTypeIdentifiers', () { + const List uniformTypeIdentifiers = [ + 'public.plain-text' + ]; + const XTypeGroup group = XTypeGroup( + macUTIs: uniformTypeIdentifiers, + ); + + expect(group.uniformTypeIdentifiers, uniformTypeIdentifiers); + }); + + test( + 'passing only uniformTypeIdentifiers should fill uniformTypeIdentifiers', + () { + const List uniformTypeIdentifiers = [ + 'public.plain-text' + ]; + const XTypeGroup group = XTypeGroup( + uniformTypeIdentifiers: uniformTypeIdentifiers, + ); + + expect(group.uniformTypeIdentifiers, uniformTypeIdentifiers); + }); + + test('macUTIs getter return macUTIs value passed in constructor', () { + const List uniformTypeIdentifiers = [ + 'public.plain-text' + ]; + const XTypeGroup group = XTypeGroup( + macUTIs: uniformTypeIdentifiers, + ); + + expect(group.macUTIs, uniformTypeIdentifiers); + }); + + test( + 'macUTIs getter returns uniformTypeIdentifiers value passed in constructor', + () { + const List uniformTypeIdentifiers = [ + 'public.plain-text' + ]; + const XTypeGroup group = XTypeGroup( + uniformTypeIdentifiers: uniformTypeIdentifiers, + ); + + expect(group.macUTIs, uniformTypeIdentifiers); + }); + + test('passing both uniformTypeIdentifiers and macUTIs should throw', () { + expect( + () => XTypeGroup( + macUTIs: const ['public.plain-text'], + uniformTypeIdentifiers: const ['public.plain-images']), + throwsA(predicate((Object? e) => + e is AssertionError && + e.message == + 'Only one of uniformTypeIdentifiers or macUTIs can be non-null'))); + }); + + test( + 'having uniformTypeIdentifiers and macUTIs as null should leave uniformTypeIdentifiers as null', + () { + const XTypeGroup group = XTypeGroup(); + + expect(group.uniformTypeIdentifiers, null); + }); }); test('leading dots are removed from extensions', () { diff --git a/packages/file_selector/file_selector_web/CHANGELOG.md b/packages/file_selector/file_selector_web/CHANGELOG.md index d456e13f97e8..43ad4962f044 100644 --- a/packages/file_selector/file_selector_web/CHANGELOG.md +++ b/packages/file_selector/file_selector_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 0.9.0+4 * Clarifies explanation of endorsement in README. diff --git a/packages/file_selector/file_selector_web/example/pubspec.yaml b/packages/file_selector/file_selector_web/example/pubspec.yaml index 9773e2306088..bc8b984e367d 100644 --- a/packages/file_selector/file_selector_web/example/pubspec.yaml +++ b/packages/file_selector/file_selector_web/example/pubspec.yaml @@ -2,8 +2,8 @@ name: file_selector_web_integration_tests publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: file_selector_platform_interface: ^2.2.0 diff --git a/packages/file_selector/file_selector_web/pubspec.yaml b/packages/file_selector/file_selector_web/pubspec.yaml index 9c0693f0d777..8a601cbb3668 100644 --- a/packages/file_selector/file_selector_web/pubspec.yaml +++ b/packages/file_selector/file_selector_web/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 0.9.0+4 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: @@ -17,7 +17,7 @@ flutter: fileName: file_selector_web.dart dependencies: - file_selector_platform_interface: ^2.2.0 + file_selector_platform_interface: ^2.3.0 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/file_selector/file_selector_web/test/utils_test.dart b/packages/file_selector/file_selector_web/test/utils_test.dart index f9f3a41295f0..e207f3d45df5 100644 --- a/packages/file_selector/file_selector_web/test/utils_test.dart +++ b/packages/file_selector/file_selector_web/test/utils_test.dart @@ -56,7 +56,8 @@ void main() { test('throws for a type group that does not support web', () { const List acceptedTypes = [ - XTypeGroup(label: 'text', macUTIs: ['public.text']), + XTypeGroup( + label: 'text', uniformTypeIdentifiers: ['public.text']), ]; expect(() => acceptedTypesToString(acceptedTypes), throwsArgumentError); }); 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 c7a380d12028..fbe3683af37d 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 @@ -51,14 +51,13 @@ void main() { label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], - macUTIs: ['public.text'], ); const XTypeGroup groupTwo = XTypeGroup( - label: 'image', - extensions: ['jpg'], - mimeTypes: ['image/jpg'], - macUTIs: ['public.image']); + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + ); await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); @@ -129,14 +128,13 @@ void main() { label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], - macUTIs: ['public.text'], ); const XTypeGroup groupTwo = XTypeGroup( - label: 'image', - extensions: ['jpg'], - mimeTypes: ['image/jpg'], - macUTIs: ['public.image']); + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + ); await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); @@ -266,14 +264,13 @@ void main() { label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], - macUTIs: ['public.text'], ); const XTypeGroup groupTwo = XTypeGroup( - label: 'image', - extensions: ['jpg'], - mimeTypes: ['image/jpg'], - macUTIs: ['public.image']); + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + ); await plugin .getSavePath(acceptedTypeGroups: [group, groupTwo]); diff --git a/packages/flutter_adaptive_scaffold/CHANGELOG.md b/packages/flutter_adaptive_scaffold/CHANGELOG.md index 793682b7787b..6ff69f593d36 100644 --- a/packages/flutter_adaptive_scaffold/CHANGELOG.md +++ b/packages/flutter_adaptive_scaffold/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.4 + +* Use Material 3 NavigationBar instead of BottomNavigationBar + ## 0.1.3 * Fixes `groupAlignment` property not available in `standardNavigationRail` - [flutter/flutter#121994](https://github.com/flutter/flutter/issues/121994) diff --git a/packages/flutter_adaptive_scaffold/example/.pluginToolsConfig.yaml b/packages/flutter_adaptive_scaffold/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/flutter_adaptive_scaffold/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/flutter_adaptive_scaffold/example/macos/Runner.xcodeproj/project.pbxproj b/packages/flutter_adaptive_scaffold/example/macos/Runner.xcodeproj/project.pbxproj index 736ab66eb763..d9333e4704c4 100644 --- a/packages/flutter_adaptive_scaffold/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/flutter_adaptive_scaffold/example/macos/Runner.xcodeproj/project.pbxproj @@ -345,7 +345,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -424,7 +424,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -471,7 +471,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/flutter_adaptive_scaffold/example/test/adaptive_scaffold_demo_test.dart b/packages/flutter_adaptive_scaffold/example/test/adaptive_scaffold_demo_test.dart index 41df6dffcfdf..c22f9ebab733 100644 --- a/packages/flutter_adaptive_scaffold/example/test/adaptive_scaffold_demo_test.dart +++ b/packages/flutter_adaptive_scaffold/example/test/adaptive_scaffold_demo_test.dart @@ -33,7 +33,7 @@ void main() { expect(smallBody, findsOneWidget); expect(bnav, findsOneWidget); expect(tester.getTopLeft(smallBody), Offset.zero); - expect(tester.getTopLeft(bnav), const Offset(0, 744)); + expect(tester.getTopLeft(bnav), const Offset(0, 720)); expect(body, findsNothing); expect(largeBody, findsNothing); expect(pnav, findsNothing); @@ -73,22 +73,22 @@ void main() { expect(tester.getTopLeft(b), const Offset(17.6, 0)); expect(tester.getBottomRight(b), - offsetMoreOrLessEquals(const Offset(778.2, 755.2), epsilon: 1.0)); + offsetMoreOrLessEquals(const Offset(778.2, 736), epsilon: 1.0)); expect(tester.getTopLeft(sBody), offsetMoreOrLessEquals(const Offset(778.2, 0), epsilon: 1.0)); expect(tester.getBottomRight(sBody), - offsetMoreOrLessEquals(const Offset(1178.2, 755.2), epsilon: 1.0)); + offsetMoreOrLessEquals(const Offset(1178.2, 736), epsilon: 1.0)); await tester.pump(); await tester.pump(const Duration(milliseconds: 600)); expect(tester.getTopLeft(b), const Offset(70.4, 0)); expect(tester.getBottomRight(b), - offsetMoreOrLessEquals(const Offset(416.0, 788.8), epsilon: 1.0)); + offsetMoreOrLessEquals(const Offset(416.0, 784), epsilon: 1.0)); expect(tester.getTopLeft(sBody), offsetMoreOrLessEquals(const Offset(416, 0), epsilon: 1.0)); expect(tester.getBottomRight(sBody), - offsetMoreOrLessEquals(const Offset(816, 788.8), epsilon: 1.0)); + offsetMoreOrLessEquals(const Offset(816, 784), epsilon: 1.0)); await tester.pump(); await tester.pump(const Duration(milliseconds: 200)); diff --git a/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart b/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart index 3eb9ee6435e4..028f47e7cad7 100644 --- a/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart +++ b/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart @@ -316,15 +316,24 @@ class AdaptiveScaffold extends StatefulWidget { ValueChanged? onDestinationSelected, }) { return Builder( - builder: (_) { - return BottomNavigationBar( - currentIndex: currentIndex ?? 0, - iconSize: iconSize, - items: destinations - .map((NavigationDestination e) => _toBottomNavItem(e)) - .toList(), - onTap: onDestinationSelected, - ); + builder: (BuildContext context) { + final NavigationBarThemeData currentNavBarTheme = + NavigationBarTheme.of(context); + return NavigationBarTheme( + data: currentNavBarTheme.copyWith( + iconTheme: MaterialStateProperty.resolveWith( + (Set states) { + return currentNavBarTheme.iconTheme + ?.resolve(states) + ?.copyWith(size: iconSize) ?? + IconTheme.of(context).copyWith(size: iconSize); + }), + ), + child: NavigationBar( + selectedIndex: currentIndex ?? 0, + destinations: destinations, + onDestinationSelected: onDestinationSelected, + )); }, ); } @@ -644,14 +653,6 @@ class _AdaptiveScaffoldState extends State { } } -BottomNavigationBarItem _toBottomNavItem(NavigationDestination destination) { - return BottomNavigationBarItem( - label: destination.label, - icon: destination.icon, - activeIcon: destination.selectedIcon, - ); -} - class _BrickLayout extends StatelessWidget { const _BrickLayout({ this.columns = 1, diff --git a/packages/flutter_adaptive_scaffold/pubspec.yaml b/packages/flutter_adaptive_scaffold/pubspec.yaml index 8157ffb287b7..d988420a2221 100644 --- a/packages/flutter_adaptive_scaffold/pubspec.yaml +++ b/packages/flutter_adaptive_scaffold/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_adaptive_scaffold description: Widgets to easily build adaptive layouts, including navigation elements. -version: 0.1.3 +version: 0.1.4 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_adaptive_scaffold%22 repository: https://github.com/flutter/packages/tree/main/packages/flutter_adaptive_scaffold diff --git a/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart b/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart index 65035389659e..325628645522 100644 --- a/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart +++ b/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart @@ -33,7 +33,7 @@ void main() { expect(tester.getTopLeft(smallBody), Offset.zero); expect(tester.getTopLeft(smallSBody), const Offset(200, 0)); - expect(tester.getTopLeft(bottomNav), const Offset(0, 744)); + expect(tester.getTopLeft(bottomNav), const Offset(0, 720)); await tester.binding.setSurfaceSize(SimulatedLayout.medium.size); await tester.pumpWidget(SimulatedLayout.medium.app()); @@ -83,22 +83,22 @@ void main() { expect(tester.getTopLeft(b), const Offset(17.6, 0)); expect(tester.getBottomRight(b), - offsetMoreOrLessEquals(const Offset(778.2, 755.2), epsilon: 1.0)); + offsetMoreOrLessEquals(const Offset(778.2, 736), epsilon: 1.0)); expect(tester.getTopLeft(sBody), offsetMoreOrLessEquals(const Offset(778.2, 0), epsilon: 1.0)); expect(tester.getBottomRight(sBody), - offsetMoreOrLessEquals(const Offset(1178.2, 755.2), epsilon: 1.0)); + offsetMoreOrLessEquals(const Offset(1178.2, 736), epsilon: 1.0)); await tester.pump(); await tester.pump(const Duration(milliseconds: 600)); expect(tester.getTopLeft(b), const Offset(70.4, 0)); expect(tester.getBottomRight(b), - offsetMoreOrLessEquals(const Offset(416.0, 788.8), epsilon: 1.0)); + offsetMoreOrLessEquals(const Offset(416.0, 784), epsilon: 1.0)); expect(tester.getTopLeft(sBody), offsetMoreOrLessEquals(const Offset(416, 0), epsilon: 1.0)); expect(tester.getBottomRight(sBody), - offsetMoreOrLessEquals(const Offset(816, 788.8), epsilon: 1.0)); + offsetMoreOrLessEquals(const Offset(816, 784), epsilon: 1.0)); await tester.pump(); await tester.pump(const Duration(milliseconds: 200)); diff --git a/packages/flutter_image/CHANGELOG.md b/packages/flutter_image/CHANGELOG.md index 8db895a71bd3..a4b65bb17758 100644 --- a/packages/flutter_image/CHANGELOG.md +++ b/packages/flutter_image/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. * Aligns Dart and Flutter SDK constraints. ## 4.1.5 diff --git a/packages/flutter_image/pubspec.yaml b/packages/flutter_image/pubspec.yaml index 539576a3494d..1021c8a1ba93 100644 --- a/packages/flutter_image/pubspec.yaml +++ b/packages/flutter_image/pubspec.yaml @@ -6,8 +6,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 4.1.5 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/flutter_lints/CHANGELOG.md b/packages/flutter_lints/CHANGELOG.md index f94b1beff1bc..4af34c1727ff 100644 --- a/packages/flutter_lints/CHANGELOG.md +++ b/packages/flutter_lints/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.0.1 * Updated readme to document suggestion process for new lints diff --git a/packages/flutter_lints/example/pubspec.yaml b/packages/flutter_lints/example/pubspec.yaml index 2ed661fe2dca..1f474eb1a5b4 100644 --- a/packages/flutter_lints/example/pubspec.yaml +++ b/packages/flutter_lints/example/pubspec.yaml @@ -4,7 +4,7 @@ description: A project that showcases how to enable the recommended lints for Fl publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=2.18.0 <4.0.0" # Add the latest version of `package:flutter_lints` as a dev_dependency. The # lint set provided by this package is activated in the `analysis_options.yaml` diff --git a/packages/flutter_lints/pubspec.yaml b/packages/flutter_lints/pubspec.yaml index 69ae368abd17..0f7505d6f0dd 100644 --- a/packages/flutter_lints/pubspec.yaml +++ b/packages/flutter_lints/pubspec.yaml @@ -5,7 +5,7 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.0.1 environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=2.18.0 <4.0.0" dependencies: lints: ^2.0.0 diff --git a/packages/flutter_markdown/CHANGELOG.md b/packages/flutter_markdown/CHANGELOG.md index c9376d87dc08..1c28f585f94b 100644 --- a/packages/flutter_markdown/CHANGELOG.md +++ b/packages/flutter_markdown/CHANGELOG.md @@ -2,6 +2,7 @@ * Updates minimum Flutter version to 3.3. * Aligns Dart and Flutter SDK constraints. +* Replace `describeEnum` with the `name` getter. ## 0.6.14 diff --git a/packages/flutter_markdown/example/.pluginToolsConfig.yaml b/packages/flutter_markdown/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/flutter_markdown/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/flutter_markdown/example/android/build.gradle b/packages/flutter_markdown/example/android/build.gradle index 4b30292ebe1f..ce647a433bd0 100644 --- a/packages/flutter_markdown/example/android/build.gradle +++ b/packages/flutter_markdown/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.21' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() diff --git a/packages/flutter_markdown/example/ios/Flutter/AppFrameworkInfo.plist b/packages/flutter_markdown/example/ios/Flutter/AppFrameworkInfo.plist index 9367d483e44e..9625e105df39 100644 --- a/packages/flutter_markdown/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/flutter_markdown/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 11.0 diff --git a/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.pbxproj b/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.pbxproj index e757aaeab9d9..8b5ead8ee383 100644 --- a/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/flutter_markdown/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -127,7 +127,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -171,6 +171,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -185,6 +186,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -272,7 +274,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -346,7 +348,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -395,7 +397,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/flutter_markdown/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/flutter_markdown/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cfdb3f..3db53b6e1fb7 100644 --- a/packages/flutter_markdown/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/flutter_markdown/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/flutter_markdown/example/lib/shared/markdown_extensions.dart b/packages/flutter_markdown/example/lib/shared/markdown_extensions.dart index bb2daa1e3e18..88cbf3cca0a0 100644 --- a/packages/flutter_markdown/example/lib/shared/markdown_extensions.dart +++ b/packages/flutter_markdown/example/lib/shared/markdown_extensions.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:markdown/markdown.dart' as md; @@ -11,8 +10,6 @@ import 'package:markdown/markdown.dart' as md; enum MarkdownExtensionSet { none, commonMark, githubFlavored, githubWeb } extension MarkdownExtensionSetExtension on MarkdownExtensionSet { - String get name => describeEnum(this); - String get displayTitle => () { switch (this) { case MarkdownExtensionSet.none: @@ -41,8 +38,6 @@ extension MarkdownExtensionSetExtension on MarkdownExtensionSet { } extension WrapAlignmentExtension on WrapAlignment { - String get name => describeEnum(this); - String get displayTitle => () { switch (this) { case WrapAlignment.center: diff --git a/packages/flutter_markdown/example/macos/Runner.xcodeproj/project.pbxproj b/packages/flutter_markdown/example/macos/Runner.xcodeproj/project.pbxproj index 39be746092ca..acd4877da18c 100644 --- a/packages/flutter_markdown/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/flutter_markdown/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -182,7 +182,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -235,6 +235,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -344,7 +345,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -423,7 +424,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -470,7 +471,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/flutter_markdown/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/flutter_markdown/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 74c450c90f3b..f38fdfa8cf6a 100644 --- a/packages/flutter_markdown/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/flutter_markdown/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ >{'assets/logo.png': []})!; return Future.value(manifest); + } else if (key == 'AssetManifest.smcbin') { + final ByteData manifest = const StandardMessageCodec().encodeMessage( + >{'assets/logo.png': []})!; + return Future.value(manifest); } else if (key == 'assets/logo.png') { // The root directory tests are run from is different for 'flutter test' // verses 'flutter test test/*_test.dart'. Adjust the root directory @@ -204,9 +208,6 @@ class TestAssetBundle extends CachingAssetBundle { io.File('${rootDirectory.path}/test/assets/images/logo.png'); final ByteData asset = ByteData.view(file.readAsBytesSync().buffer); - if (asset == null) { - throw FlutterError('Unable to load asset: $key'); - } return asset; } else { throw ArgumentError('Unknown asset key: $key'); diff --git a/packages/flutter_migrate/CHANGELOG.md b/packages/flutter_migrate/CHANGELOG.md index 5aaff1c1f71c..f71225f718c7 100644 --- a/packages/flutter_migrate/CHANGELOG.md +++ b/packages/flutter_migrate/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.1+3 + +* Removes obsolete null checks on non-nullable values. + ## 0.0.1+2 * Removes use of `runtimeType.toString()`. diff --git a/packages/flutter_migrate/lib/src/base/logger.dart b/packages/flutter_migrate/lib/src/base/logger.dart index c84f098af7fd..70ff228a3901 100644 --- a/packages/flutter_migrate/lib/src/base/logger.dart +++ b/packages/flutter_migrate/lib/src/base/logger.dart @@ -708,7 +708,6 @@ class BufferLogger extends Logger { String? progressId, int progressIndicatorPadding = kDefaultStatusPadding, }) { - assert(progressIndicatorPadding != null); printStatus(message); return SilentStatus( stopwatch: _stopwatchFactory.createStopwatch(), @@ -983,7 +982,6 @@ class AnonymousSpinnerStatus extends Status { void _callback(Timer timer) { assert(this.timer == timer); - assert(timer != null); assert(timer.isActive); _writeToStdOut(_backspaceChar * _lastAnimationFrameLength); ticks += 1; @@ -1147,7 +1145,7 @@ String wrapText( int? indent, }) { assert(columnWidth >= 0); - if (text == null || text.isEmpty) { + if (text.isEmpty) { return ''; } indent ??= 0; @@ -1231,7 +1229,7 @@ List _wrapTextAsLines( required int columnWidth, required bool shouldWrap, }) { - if (text == null || text.isEmpty) { + if (text.isEmpty) { return ['']; } assert(start >= 0); diff --git a/packages/flutter_migrate/lib/src/base/project.dart b/packages/flutter_migrate/lib/src/base/project.dart index b8158d6f5aa0..9aa2cc67fe65 100644 --- a/packages/flutter_migrate/lib/src/base/project.dart +++ b/packages/flutter_migrate/lib/src/base/project.dart @@ -27,7 +27,6 @@ class FlutterProjectFactory { /// Returns a [FlutterProject] view of the given directory or a ToolExit error, /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid. FlutterProject fromDirectory(Directory directory) { - assert(directory != null); return projects.putIfAbsent(directory.path, () { return FlutterProject(directory); }); @@ -36,7 +35,7 @@ class FlutterProjectFactory { /// Represents the contents of a Flutter project at the specified [directory]. class FlutterProject { - FlutterProject(this.directory) : assert(directory != null); + FlutterProject(this.directory); /// Returns a [FlutterProject] view of the current directory or a ToolExit error, /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid. diff --git a/packages/flutter_migrate/lib/src/base/terminal.dart b/packages/flutter_migrate/lib/src/base/terminal.dart index aeb8f2812fd6..8bb5480245fa 100644 --- a/packages/flutter_migrate/lib/src/base/terminal.dart +++ b/packages/flutter_migrate/lib/src/base/terminal.dart @@ -235,7 +235,6 @@ class AnsiTerminal implements Terminal { @override String bolden(String message) { - assert(message != null); if (!supportsColor || message.isEmpty) { return message; } @@ -256,8 +255,7 @@ class AnsiTerminal implements Terminal { @override String color(String message, TerminalColor color) { - assert(message != null); - if (!supportsColor || color == null || message.isEmpty) { + if (!supportsColor || message.isEmpty) { return message; } final StringBuffer buffer = StringBuffer(); diff --git a/packages/flutter_migrate/lib/src/flutter_project_metadata.dart b/packages/flutter_migrate/lib/src/flutter_project_metadata.dart index 281ec8ed9b7e..d8f4026ba11c 100644 --- a/packages/flutter_migrate/lib/src/flutter_project_metadata.dart +++ b/packages/flutter_migrate/lib/src/flutter_project_metadata.dart @@ -195,12 +195,6 @@ class FlutterProjectMetadata { /// needs to be able to write the .migrate_config file into legacy apps. void writeFile({File? outputFile}) { outputFile = outputFile ?? file; - if (outputFile == null) { - // In-memory FlutterProjectMetadata instances requires an output file to - // be passed or specified in the constructor. - throw const FileSystemException( - 'No outputFile specified to write .metadata to. Initialize with a file or provide one when writing.'); - } outputFile ..createSync(recursive: true) ..writeAsStringSync(toString(), flush: true); diff --git a/packages/flutter_migrate/pubspec.yaml b/packages/flutter_migrate/pubspec.yaml index 6b7ea837663a..8d5ee02c668c 100644 --- a/packages/flutter_migrate/pubspec.yaml +++ b/packages/flutter_migrate/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_migrate description: A tool to migrate legacy flutter projects to modern versions. -version: 0.0.1+2 +version: 0.0.1+3 repository: https://github.com/flutter/packages/tree/main/packages/flutter_migrate issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Ap%3A%20flutter_migrate publish_to: none diff --git a/packages/flutter_migrate/test/src/fakes.dart b/packages/flutter_migrate/test/src/fakes.dart index b78ed64bca72..498213a6f7b0 100644 --- a/packages/flutter_migrate/test/src/fakes.dart +++ b/packages/flutter_migrate/test/src/fakes.dart @@ -127,7 +127,6 @@ class MemoryStdout extends MemoryIOSink implements io.Stdout { @override bool get hasTerminal => _hasTerminal; set hasTerminal(bool value) { - assert(value != null); _hasTerminal = value; } @@ -139,7 +138,6 @@ class MemoryStdout extends MemoryIOSink implements io.Stdout { @override bool get supportsAnsiEscapes => _supportsAnsiEscapes; set supportsAnsiEscapes(bool value) { - assert(value != null); _supportsAnsiEscapes = value; } diff --git a/packages/flutter_migrate/test/test_data/migrate_project.dart b/packages/flutter_migrate/test/test_data/migrate_project.dart index 90d68890bc22..e62afdfc58ad 100644 --- a/packages/flutter_migrate/test/test_data/migrate_project.dart +++ b/packages/flutter_migrate/test/test_data/migrate_project.dart @@ -67,10 +67,8 @@ class MigrateProject extends Project { }) async { this.dir = dir; _appPath = dir.path; - if (androidLocalProperties != null) { - writeFile(fileSystem.path.join(dir.path, 'android', 'local.properties'), - androidLocalProperties); - } + writeFile(fileSystem.path.join(dir.path, 'android', 'local.properties'), + androidLocalProperties); final Directory tempDir = createResolvedTempDirectorySync('cipd_dest.'); final Directory depotToolsDir = createResolvedTempDirectorySync('depot_tools.'); diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md index 1e1e0316a471..85e6fb1d0a60 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2.0.15 + +* Fixes Java lints. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 2.0.14 + +* Fixes compatibility with ActivityPluginBinding. + ## 2.0.13 * Fixes compatibility with AGP versions older than 4.2. diff --git a/packages/flutter_plugin_android_lifecycle/android/build.gradle b/packages/flutter_plugin_android_lifecycle/android/build.gradle index 5a1933069cc1..9ec027f33655 100644 --- a/packages/flutter_plugin_android_lifecycle/android/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/android/build.gradle @@ -43,7 +43,6 @@ android { checkAllWarnings true warningsAsErrors true disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' - baseline file("lint-baseline.xml") } dependencies { @@ -68,4 +67,3 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:5.1.1' } - diff --git a/packages/flutter_plugin_android_lifecycle/android/lint-baseline.xml b/packages/flutter_plugin_android_lifecycle/android/lint-baseline.xml deleted file mode 100644 index 0c5fc18e5a6f..000000000000 --- a/packages/flutter_plugin_android_lifecycle/android/lint-baseline.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - diff --git a/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapter.java b/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapter.java index 05490eb93e46..9251f9da6d8a 100644 --- a/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapter.java +++ b/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapter.java @@ -10,8 +10,6 @@ /** Provides a static method for extracting lifecycle objects from Flutter plugin bindings. */ public class FlutterLifecycleAdapter { - private static final String TAG = "FlutterLifecycleAdapter"; - /** * Returns the lifecycle object for the activity a plugin is bound to. * diff --git a/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle/FlutterAndroidLifecyclePlugin.java b/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle/FlutterAndroidLifecyclePlugin.java index e3b8ea2a6318..800f3b594d61 100644 --- a/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle/FlutterAndroidLifecyclePlugin.java +++ b/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle/FlutterAndroidLifecyclePlugin.java @@ -15,7 +15,8 @@ */ public class FlutterAndroidLifecyclePlugin implements FlutterPlugin { @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) { // no-op } diff --git a/packages/flutter_plugin_android_lifecycle/android/src/test/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapterTest.java b/packages/flutter_plugin_android_lifecycle/android/src/test/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapterTest.java index 9a6bfb7da5ba..80bfc4ca5624 100644 --- a/packages/flutter_plugin_android_lifecycle/android/src/test/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapterTest.java +++ b/packages/flutter_plugin_android_lifecycle/android/src/test/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapterTest.java @@ -5,12 +5,10 @@ package io.flutter.embedding.engine.plugins.lifecycle; import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; -import android.app.Activity; -import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.PluginRegistry; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -19,6 +17,7 @@ public class FlutterLifecycleAdapterTest { @Mock Lifecycle lifecycle; + @Mock ActivityPluginBinding mockActivityPluginBinding; AutoCloseable mockCloseable; @@ -34,66 +33,14 @@ public void tearDown() throws Exception { @Test public void getActivityLifecycle() { - TestActivityPluginBinding binding = new TestActivityPluginBinding(lifecycle); + when(mockActivityPluginBinding.getLifecycle()) + .thenReturn(new HiddenLifecycleReference(lifecycle)); - Lifecycle parsedLifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); + when(mockActivityPluginBinding.getActivity()).thenReturn(null); - assertEquals(lifecycle, parsedLifecycle); - } - - private static final class TestActivityPluginBinding implements ActivityPluginBinding { - private final Lifecycle lifecycle; - - TestActivityPluginBinding(Lifecycle lifecycle) { - this.lifecycle = lifecycle; - } - - @NonNull - public Object getLifecycle() { - return new HiddenLifecycleReference(lifecycle); - } - - @Override - public Activity getActivity() { - return null; - } - - @Override - public void addRequestPermissionsResultListener( - @NonNull PluginRegistry.RequestPermissionsResultListener listener) {} - - @Override - public void removeRequestPermissionsResultListener( - @NonNull PluginRegistry.RequestPermissionsResultListener listener) {} + Lifecycle parsedLifecycle = + FlutterLifecycleAdapter.getActivityLifecycle(mockActivityPluginBinding); - @Override - public void addActivityResultListener( - @NonNull PluginRegistry.ActivityResultListener listener) {} - - @Override - public void removeActivityResultListener( - @NonNull PluginRegistry.ActivityResultListener listener) {} - - @Override - public void addOnNewIntentListener(@NonNull PluginRegistry.NewIntentListener listener) {} - - @Override - public void removeOnNewIntentListener(@NonNull PluginRegistry.NewIntentListener listener) {} - - @Override - public void addOnUserLeaveHintListener( - @NonNull PluginRegistry.UserLeaveHintListener listener) {} - - @Override - public void removeOnUserLeaveHintListener( - @NonNull PluginRegistry.UserLeaveHintListener listener) {} - - @Override - public void addOnSaveStateListener( - @NonNull ActivityPluginBinding.OnSaveInstanceStateListener listener) {} - - @Override - public void removeOnSaveStateListener( - @NonNull ActivityPluginBinding.OnSaveInstanceStateListener listener) {} + assertEquals(lifecycle, parsedLifecycle); } } diff --git a/packages/flutter_plugin_android_lifecycle/example/.pluginToolsConfig.yaml b/packages/flutter_plugin_android_lifecycle/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/flutter_plugin_android_lifecycle/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml index abb0c2b9a6c3..c012f3890b1c 100644 --- a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the flutter_plugin_android_lifecycle plugin publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/flutter_plugin_android_lifecycle/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/pubspec.yaml index fd111c234da3..afdb5d980594 100644 --- a/packages/flutter_plugin_android_lifecycle/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/pubspec.yaml @@ -2,11 +2,11 @@ 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.13 +version: 2.0.15 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/flutter_template_images/CHANGELOG.md b/packages/flutter_template_images/CHANGELOG.md index 8c49d1fba04c..2e17699eed8a 100644 --- a/packages/flutter_template_images/CHANGELOG.md +++ b/packages/flutter_template_images/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. * Updates minimum SDK version to Flutter 3.0. ## 4.2.0 diff --git a/packages/flutter_template_images/pubspec.yaml b/packages/flutter_template_images/pubspec.yaml index f5d4abbf8032..d1bebb83a5ef 100644 --- a/packages/flutter_template_images/pubspec.yaml +++ b/packages/flutter_template_images/pubspec.yaml @@ -5,4 +5,4 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 4.2.0 environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=2.18.0 <4.0.0" diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index d46e9de8c32e..4a0d29b6c6a7 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,22 @@ +## 7.1.1 + +* Removes obsolete null checks on non-nullable values. + +## 7.1.0 + +- Introduces `StatefulShellRoute` to support using separate navigators for child routes as well as preserving state in each navigation tree (flutter/flutter#99124). +- Updates documentation for `pageBuilder` and `builder` fields of `ShellRoute`, to more correctly + describe the meaning of the child argument in the builder functions. +- Adds support for restorationId to ShellRoute (and StatefulShellRoute). + +## 7.0.2 + +- Fixes `BuildContext` extension method `replaceNamed` to correctly pass `pathParameters` and `queryParameters`. + +## 7.0.1 + +- Adds a workaround for the `dart fix --apply` issue, https://github.com/dart-lang/sdk/issues/52233. + ## 7.0.0 - **BREAKING CHANGE**: @@ -24,7 +43,7 @@ ## 6.5.7 -- Fixes a bug that go_router would crash if `GoRoute.pageBuilder` depends on `InheritedWidget`s. +- Fixes a bug that go_router would crash if `GoRoute.pageBuilder` depends on `InheritedWidget`s. ## 6.5.6 diff --git a/packages/go_router/example/.pluginToolsConfig.yaml b/packages/go_router/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/go_router/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index 90c7aad18a53..f4c1c0f48d35 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -30,6 +30,12 @@ An example to demonstrate how to use redirect to handle a synchronous sign-in fl An example to demonstrate how to use handle a sign-in flow with a stream authentication service. +## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart) +`flutter run lib/stacked_shell_route.dart` + +An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a +`BottomNavigationBar`. + ## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books) `flutter run lib/books/main.dart` diff --git a/packages/go_router/example/android/build.gradle b/packages/go_router/example/android/build.gradle index 96e940b06ea2..586557f0afcf 100644 --- a/packages/go_router/example/android/build.gradle +++ b/packages/go_router/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.21' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() diff --git a/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard b/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard index f3c28516fb38..bb612647feec 100644 --- a/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard +++ b/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard @@ -8,7 +8,7 @@ - + diff --git a/packages/go_router/example/lib/others/custom_stateful_shell_route.dart b/packages/go_router/example/lib/others/custom_stateful_shell_route.dart new file mode 100644 index 000000000000..5fbe2b6d2868 --- /dev/null +++ b/packages/go_router/example/lib/others/custom_stateful_shell_route.dart @@ -0,0 +1,460 @@ +// 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:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); +final GlobalKey _tabANavigatorKey = + GlobalKey(debugLabel: 'tabANav'); + +// This example demonstrates how to setup nested navigation using a +// BottomNavigationBar, where each bar item uses its own persistent navigator, +// i.e. navigation state is maintained separately for each item. This setup also +// enables deep linking into nested pages. +// +// This example also demonstrates how build a nested shell with a custom +// container for the branch Navigators (in this case a TabBarView). + +void main() { + runApp(NestedTabNavigationExampleApp()); +} + +/// An example demonstrating how to use nested navigators +class NestedTabNavigationExampleApp extends StatelessWidget { + /// Creates a NestedTabNavigationExampleApp + NestedTabNavigationExampleApp({super.key}); + + final GoRouter _router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/a', + routes: [ + StatefulShellRoute( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + // This nested StatefulShellRoute demonstrates the use of a + // custom container for the branch Navigators. In this implementation, + // no customization is done in the builder function (navigationShell + // itself is simply used as the Widget for the route). Instead, the + // navigatorContainerBuilder function below is provided to + // customize the container for the branch Navigators. + return navigationShell; + }, + navigatorContainerBuilder: (BuildContext context, + StatefulNavigationShell navigationShell, List children) { + // Returning a customized container for the branch + // Navigators (i.e. the `List children` argument). + // + // See ScaffoldWithNavBar for more details on how the children + // are managed (using AnimatedBranchContainer). + return ScaffoldWithNavBar( + navigationShell: navigationShell, children: children); + }, + branches: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + navigatorKey: _tabANavigatorKey, + routes: [ + GoRoute( + // The screen to display as the root in the first tab of the + // bottom navigation bar. + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const RootScreenA(), + routes: [ + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const DetailsScreen(label: 'A'), + ), + ], + ), + ], + ), + + // The route branch for the third tab of the bottom navigation bar. + StatefulShellBranch( + // StatefulShellBranch will automatically use the first descendant + // GoRoute as the initial location of the branch. If another route + // is desired, specify the location of it using the defaultLocation + // parameter. + // defaultLocation: '/c2', + routes: [ + StatefulShellRoute( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + // Just like with the top level StatefulShellRoute, no + // customization is done in the builder function. + return navigationShell; + }, + navigatorContainerBuilder: (BuildContext context, + StatefulNavigationShell navigationShell, + List children) { + // Returning a customized container for the branch + // Navigators (i.e. the `List children` argument). + // + // See TabbedRootScreen for more details on how the children + // are managed (in a TabBarView). + return TabbedRootScreen( + navigationShell: navigationShell, children: children); + }, + // This bottom tab uses a nested shell, wrapping sub routes in a + // top TabBar. + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/b1', + builder: (BuildContext context, GoRouterState state) => + const TabScreen( + label: 'B1', detailsPath: '/b1/details'), + routes: [ + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + const DetailsScreen( + label: 'B1', + withScaffold: false, + ), + ), + ], + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b2', + builder: (BuildContext context, GoRouterState state) => + const TabScreen( + label: 'B2', detailsPath: '/b2/details'), + routes: [ + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + const DetailsScreen( + label: 'B2', + withScaffold: false, + ), + ), + ], + ), + ]), + ], + ), + ], + ), + ], + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.navigationShell, + required this.children, + Key? key, + }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + + /// The navigation shell and container for the branch Navigators. + final StatefulNavigationShell navigationShell; + + /// The children (branch Navigators) to display in a custom container + /// ([AnimatedBranchContainer]). + final List children; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: AnimatedBranchContainer( + currentIndex: navigationShell.currentIndex, + children: children, + ), + bottomNavigationBar: BottomNavigationBar( + // Here, the items of BottomNavigationBar are hard coded. In a real + // world scenario, the items would most likely be generated from the + // branches of the shell route, which can be fetched using + // `navigationShell.route.branches`. + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), + BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), + ], + currentIndex: navigationShell.currentIndex, + onTap: (int index) => _onTap(context, index), + ), + ); + } + + /// Navigate to the current location of the branch at the provided index when + /// tapping an item in the BottomNavigationBar. + void _onTap(BuildContext context, int index) { + // When navigating to a new branch, it's recommended to use the goBranch + // method, as doing so makes sure the last navigation state of the + // Navigator for the branch is restored. + navigationShell.goBranch( + index, + // A common pattern when using bottom navigation bars is to support + // navigating to the initial location when tapping the item that is + // already active. This example demonstrates how to support this behavior, + // using the initialLocation parameter of goBranch. + initialLocation: index == navigationShell.currentIndex, + ); + } +} + +/// Custom branch Navigator container that provides animated transitions +/// when switching branches. +class AnimatedBranchContainer extends StatelessWidget { + /// Creates a AnimatedBranchContainer + const AnimatedBranchContainer( + {super.key, required this.currentIndex, required this.children}); + + /// The index (in [children]) of the branch Navigator to display. + final int currentIndex; + + /// The children (branch Navigators) to display in this container. + final List children; + + @override + Widget build(BuildContext context) { + return Stack( + children: children.mapIndexed( + (int index, Widget navigator) { + return AnimatedScale( + scale: index == currentIndex ? 1 : 1.5, + duration: const Duration(milliseconds: 400), + child: AnimatedOpacity( + opacity: index == currentIndex ? 1 : 0, + duration: const Duration(milliseconds: 400), + child: _branchNavigatorWrapper(index, navigator), + ), + ); + }, + ).toList()); + } + + Widget _branchNavigatorWrapper(int index, Widget navigator) => IgnorePointer( + ignoring: index != currentIndex, + child: TickerMode( + enabled: index == currentIndex, + child: navigator, + ), + ); +} + +/// Widget for the root page for the first section of the bottom navigation bar. +class RootScreenA extends StatelessWidget { + /// Creates a RootScreenA + const RootScreenA({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Root of section A'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen A', style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go('/a/details'); + }, + child: const Text('View details'), + ), + ], + ), + ), + ); + } +} + +/// The details screen for either the A or B screen. +class DetailsScreen extends StatefulWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + this.param, + this.withScaffold = true, + super.key, + }); + + /// The label to display in the center of the screen. + final String label; + + /// Optional param + final String? param; + + /// Wrap in scaffold + final bool withScaffold; + + @override + State createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State { + int _counter = 0; + + @override + Widget build(BuildContext context) { + if (widget.withScaffold) { + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: _build(context), + ); + } else { + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: _build(context), + ); + } + } + + Widget _build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Details for ${widget.label} - Counter: $_counter', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + setState(() { + _counter++; + }); + }, + child: const Text('Increment counter'), + ), + const Padding(padding: EdgeInsets.all(8)), + if (widget.param != null) + Text('Parameter: ${widget.param!}', + style: Theme.of(context).textTheme.titleMedium), + const Padding(padding: EdgeInsets.all(8)), + if (!widget.withScaffold) ...[ + const Padding(padding: EdgeInsets.all(16)), + TextButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + child: const Text('< Back', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + ), + ] + ], + ), + ); + } +} + +/// Builds a nested shell using a [TabBar] and [TabBarView]. +class TabbedRootScreen extends StatefulWidget { + /// Constructs a TabbedRootScreen + const TabbedRootScreen( + {required this.navigationShell, required this.children, super.key}); + + /// The current state of the parent StatefulShellRoute. + final StatefulNavigationShell navigationShell; + + /// The children (branch Navigators) to display in the [TabBarView]. + final List children; + + @override + State createState() => _TabbedRootScreenState(); +} + +class _TabbedRootScreenState extends State + with SingleTickerProviderStateMixin { + late final TabController _tabController = TabController( + length: widget.children.length, + vsync: this, + initialIndex: widget.navigationShell.currentIndex); + + @override + void didUpdateWidget(covariant TabbedRootScreen oldWidget) { + super.didUpdateWidget(oldWidget); + _tabController.index = widget.navigationShell.currentIndex; + } + + @override + Widget build(BuildContext context) { + final List tabs = widget.children + .mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}')) + .toList(); + + return Scaffold( + appBar: AppBar( + title: const Text('Root of Section B (nested TabBar shell)'), + bottom: TabBar( + controller: _tabController, + tabs: tabs, + onTap: (int tappedIndex) => _onTabTap(context, tappedIndex), + )), + body: TabBarView( + controller: _tabController, + children: widget.children, + ), + ); + } + + void _onTabTap(BuildContext context, int index) { + widget.navigationShell.goBranch(index); + } +} + +/// Widget for the pages in the top tab bar. +class TabScreen extends StatelessWidget { + /// Creates a RootScreen + const TabScreen({required this.label, required this.detailsPath, super.key}); + + /// The label + final String label; + + /// The path to the detail page + final String detailsPath; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath); + }, + child: const Text('View details'), + ), + ], + ), + ); + } +} diff --git a/packages/go_router/example/lib/others/stateful_shell_state_restoration.dart b/packages/go_router/example/lib/others/stateful_shell_state_restoration.dart new file mode 100644 index 000000000000..aeecd115590f --- /dev/null +++ b/packages/go_router/example/lib/others/stateful_shell_state_restoration.dart @@ -0,0 +1,236 @@ +// 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:go_router/go_router.dart'; + +void main() => runApp(RestorableStatefulShellRouteExampleApp()); + +/// An example demonstrating how to use StatefulShellRoute with state +/// restoration. +class RestorableStatefulShellRouteExampleApp extends StatelessWidget { + /// Creates a NestedTabNavigationExampleApp + RestorableStatefulShellRouteExampleApp({super.key}); + + final GoRouter _router = GoRouter( + initialLocation: '/a', + restorationScopeId: 'router', + routes: [ + StatefulShellRoute.indexedStack( + restorationScopeId: 'shell1', + pageBuilder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + return MaterialPage( + restorationId: 'shellWidget1', + child: ScaffoldWithNavBar(navigationShell: navigationShell)); + }, + branches: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + restorationScopeId: 'branchA', + routes: [ + GoRoute( + // The screen to display as the root in the first tab of the + // bottom navigation bar. + path: '/a', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenA', + child: + RootScreen(label: 'A', detailsPath: '/a/details')), + routes: [ + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). + GoRoute( + path: 'details', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenADetail', + child: DetailsScreen(label: 'A')), + ), + ], + ), + ], + ), + // The route branch for the second tab of the bottom navigation bar. + StatefulShellBranch( + restorationScopeId: 'branchB', + routes: [ + GoRoute( + // The screen to display as the root in the second tab of the + // bottom navigation bar. + path: '/b', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenB', + child: + RootScreen(label: 'B', detailsPath: '/b/details')), + routes: [ + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). + GoRoute( + path: 'details', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenBDetail', + child: DetailsScreen(label: 'B')), + ), + ], + ), + ], + ), + ], + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + restorationScopeId: 'app', + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.navigationShell, + Key? key, + }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + + /// The navigation shell and container for the branch Navigators. + final StatefulNavigationShell navigationShell; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: navigationShell, + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), + BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), + ], + currentIndex: navigationShell.currentIndex, + onTap: (int tappedIndex) => navigationShell.goBranch(tappedIndex), + ), + ); + } +} + +/// Widget for the root/initial pages in the bottom navigation bar. +class RootScreen extends StatelessWidget { + /// Creates a RootScreen + const RootScreen({ + required this.label, + required this.detailsPath, + super.key, + }); + + /// The label + final String label; + + /// The path to the detail page + final String detailsPath; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Root of section $label'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath); + }, + child: const Text('View details'), + ), + ], + ), + ), + ); + } +} + +/// The details screen for either the A or B screen. +class DetailsScreen extends StatefulWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + super.key, + }); + + /// The label to display in the center of the screen. + final String label; + + @override + State createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State with RestorationMixin { + final RestorableInt _counter = RestorableInt(0); + + @override + String? get restorationId => 'DetailsScreen-${widget.label}'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_counter, 'counter'); + } + + @override + void dispose() { + super.dispose(); + _counter.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: _build(context), + ); + } + + Widget _build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Details for ${widget.label} - Counter: ${_counter.value}', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + setState(() { + _counter.value++; + }); + }, + child: const Text('Increment counter'), + ), + const Padding(padding: EdgeInsets.all(8)), + ], + ), + ); + } +} diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart new file mode 100644 index 000000000000..eb0a67b679de --- /dev/null +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -0,0 +1,324 @@ +// 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:go_router/go_router.dart'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); +final GlobalKey _sectionANavigatorKey = + GlobalKey(debugLabel: 'sectionANav'); + +// This example demonstrates how to setup nested navigation using a +// BottomNavigationBar, where each bar item uses its own persistent navigator, +// i.e. navigation state is maintained separately for each item. This setup also +// enables deep linking into nested pages. + +void main() { + runApp(NestedTabNavigationExampleApp()); +} + +/// An example demonstrating how to use nested navigators +class NestedTabNavigationExampleApp extends StatelessWidget { + /// Creates a NestedTabNavigationExampleApp + NestedTabNavigationExampleApp({super.key}); + + final GoRouter _router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/a', + routes: [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + // Return the widget that implements the custom shell (in this case + // using a BottomNavigationBar). The StatefulNavigationShell is passed + // to be able access the state of the shell and to navigate to other + // branches in a stateful way. + return ScaffoldWithNavBar(navigationShell: navigationShell); + }, + branches: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + navigatorKey: _sectionANavigatorKey, + routes: [ + GoRoute( + // The screen to display as the root in the first tab of the + // bottom navigation bar. + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const RootScreen(label: 'A', detailsPath: '/a/details'), + routes: [ + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const DetailsScreen(label: 'A'), + ), + ], + ), + ], + ), + + // The route branch for the second tab of the bottom navigation bar. + StatefulShellBranch( + // It's not necessary to provide a navigatorKey if it isn't also + // needed elsewhere. If not provided, a default key will be used. + routes: [ + GoRoute( + // The screen to display as the root in the second tab of the + // bottom navigation bar. + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const RootScreen( + label: 'B', + detailsPath: '/b/details/1', + secondDetailsPath: '/b/details/2', + ), + routes: [ + GoRoute( + path: 'details/:param', + builder: (BuildContext context, GoRouterState state) => + DetailsScreen( + label: 'B', + param: state.pathParameters['param'], + ), + ), + ], + ), + ], + ), + + // The route branch for the third tab of the bottom navigation bar. + StatefulShellBranch( + routes: [ + GoRoute( + // The screen to display as the root in the third tab of the + // bottom navigation bar. + path: '/c', + builder: (BuildContext context, GoRouterState state) => + const RootScreen( + label: 'C', + detailsPath: '/c/details', + ), + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + DetailsScreen( + label: 'C', + extra: state.extra, + ), + ), + ], + ), + ], + ), + ], + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.navigationShell, + Key? key, + }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + + /// The navigation shell and container for the branch Navigators. + final StatefulNavigationShell navigationShell; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: navigationShell, + bottomNavigationBar: BottomNavigationBar( + // Here, the items of BottomNavigationBar are hard coded. In a real + // world scenario, the items would most likely be generated from the + // branches of the shell route, which can be fetched using + // `navigationShell.route.branches`. + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), + BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), + BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'), + ], + currentIndex: navigationShell.currentIndex, + onTap: (int index) => _onTap(context, index), + ), + ); + } + + /// Navigate to the current location of the branch at the provided index when + /// tapping an item in the BottomNavigationBar. + void _onTap(BuildContext context, int index) { + // When navigating to a new branch, it's recommended to use the goBranch + // method, as doing so makes sure the last navigation state of the + // Navigator for the branch is restored. + navigationShell.goBranch( + index, + // A common pattern when using bottom navigation bars is to support + // navigating to the initial location when tapping the item that is + // already active. This example demonstrates how to support this behavior, + // using the initialLocation parameter of goBranch. + initialLocation: index == navigationShell.currentIndex, + ); + } +} + +/// Widget for the root/initial pages in the bottom navigation bar. +class RootScreen extends StatelessWidget { + /// Creates a RootScreen + const RootScreen({ + required this.label, + required this.detailsPath, + this.secondDetailsPath, + super.key, + }); + + /// The label + final String label; + + /// The path to the detail page + final String detailsPath; + + /// The path to another detail page + final String? secondDetailsPath; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Root of section $label'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath, extra: '$label-XYZ'); + }, + child: const Text('View details'), + ), + const Padding(padding: EdgeInsets.all(4)), + if (secondDetailsPath != null) + TextButton( + onPressed: () { + GoRouter.of(context).go(secondDetailsPath!); + }, + child: const Text('View more details'), + ), + ], + ), + ), + ); + } +} + +/// The details screen for either the A or B screen. +class DetailsScreen extends StatefulWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + this.param, + this.extra, + this.withScaffold = true, + super.key, + }); + + /// The label to display in the center of the screen. + final String label; + + /// Optional param + final String? param; + + /// Optional extra object + final Object? extra; + + /// Wrap in scaffold + final bool withScaffold; + + @override + State createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State { + int _counter = 0; + + @override + Widget build(BuildContext context) { + if (widget.withScaffold) { + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: _build(context), + ); + } else { + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: _build(context), + ); + } + } + + Widget _build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Details for ${widget.label} - Counter: $_counter', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + setState(() { + _counter++; + }); + }, + child: const Text('Increment counter'), + ), + const Padding(padding: EdgeInsets.all(8)), + if (widget.param != null) + Text('Parameter: ${widget.param!}', + style: Theme.of(context).textTheme.titleMedium), + const Padding(padding: EdgeInsets.all(8)), + if (widget.extra != null) + Text('Extra: ${widget.extra!}', + style: Theme.of(context).textTheme.titleMedium), + if (!widget.withScaffold) ...[ + const Padding(padding: EdgeInsets.all(16)), + TextButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + child: const Text('< Back', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + ), + ] + ], + ), + ); + } +} diff --git a/packages/go_router/example/macos/Podfile b/packages/go_router/example/macos/Podfile index dade8dfad0dc..049abe295427 100644 --- a/packages/go_router/example/macos/Podfile +++ b/packages/go_router/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/go_router/example/macos/Runner.xcodeproj/project.pbxproj b/packages/go_router/example/macos/Runner.xcodeproj/project.pbxproj index 568e5338aeba..a38da0585631 100644 --- a/packages/go_router/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/go_router/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -203,7 +203,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -256,6 +256,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -404,7 +405,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -483,7 +484,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -530,7 +531,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/go_router/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/go_router/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 7c0def66ac01..23ce7c68f176 100644 --- a/packages/go_router/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/go_router/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ route, dynamic result, RouteMatch? match); + /// Builds the top-level Navigator for GoRouter. class RouteBuilder { /// [RouteBuilder] constructor. @@ -26,6 +37,7 @@ class RouteBuilder { required this.errorBuilder, required this.restorationScopeId, required this.observers, + required this.onPopPageWithRouteMatch, }); /// Builder function for a go router with Navigator. @@ -50,19 +62,18 @@ class RouteBuilder { final GoRouterStateRegistry _registry = GoRouterStateRegistry(); - final Map, RouteMatch> _routeMatchLookUp = - , RouteMatch>{}; - - /// Looks the the [RouteMatch] for a given [Page]. + /// A callback called when a `route` produced by `match` is about to be popped + /// with the `result`. /// - /// The [Page] must be in the latest [Navigator.pages]; otherwise, this method - /// returns null. - RouteMatch? getRouteMatchForPage(Page page) => - _routeMatchLookUp[page]; + /// If this method returns true, this builder pops the `route` and `match`. + /// + /// If this method returns false, this builder aborts the pop. + final PopPageWithRouteMatchCallback onPopPageWithRouteMatch; - // final Map<> /// Caches a HeroController for the nested Navigator, which solves cases where the /// Hero Widget animation stops working when navigating. + // TODO(chunhtai): Remove _goHeroCache once below issue is fixed: + // https://github.com/flutter/flutter/issues/54200 final Map, HeroController> _goHeroCache = , HeroController>{}; @@ -70,7 +81,6 @@ class RouteBuilder { Widget build( BuildContext context, RouteMatchList matchList, - PopPageCallback onPopPage, bool routerNeglect, ) { if (matchList.isEmpty) { @@ -85,14 +95,14 @@ class RouteBuilder { try { final Map, GoRouterState> newRegistry = , GoRouterState>{}; - final Widget result = tryBuild(context, matchList, onPopPage, - routerNeglect, configuration.navigatorKey, newRegistry); + final Widget result = tryBuild(context, matchList, routerNeglect, + configuration.navigatorKey, newRegistry); _registry.updateRegistry(newRegistry); return GoRouterStateRegistryScope( registry: _registry, child: result); } on _RouteBuilderError catch (e) { - return _buildErrorNavigator(context, e, matchList.uri, onPopPage, - configuration.navigatorKey); + return _buildErrorNavigator(context, e, matchList.uri, + onPopPageWithRouteMatch, configuration.navigatorKey); } }, ), @@ -107,60 +117,79 @@ class RouteBuilder { Widget tryBuild( BuildContext context, RouteMatchList matchList, - PopPageCallback onPopPage, bool routerNeglect, GlobalKey navigatorKey, Map, GoRouterState> registry, ) { + // TODO(chunhtai): move the state from local scope to a central place. + // https://github.com/flutter/flutter/issues/126365 + final _PagePopContext pagePopContext = + _PagePopContext._(onPopPageWithRouteMatch); return builderWithNav( context, _buildNavigator( - onPopPage, - buildPages(context, matchList, onPopPage, routerNeglect, navigatorKey, - registry), + pagePopContext.onPopPage, + _buildPages(context, matchList, pagePopContext, routerNeglect, + navigatorKey, registry), navigatorKey, observers: observers, + restorationScopeId: restorationScopeId, ), ); } /// Returns the top-level pages instead of the root navigator. Used for /// testing. - @visibleForTesting - List> buildPages( + List> _buildPages( BuildContext context, RouteMatchList matchList, - PopPageCallback onPopPage, + _PagePopContext pagePopContext, bool routerNeglect, GlobalKey navigatorKey, Map, GoRouterState> registry) { final Map, List>> keyToPage = , List>>{}; try { - _routeMatchLookUp.clear(); - _buildRecursive(context, matchList, 0, onPopPage, routerNeglect, + _buildRecursive(context, matchList, 0, pagePopContext, routerNeglect, keyToPage, navigatorKey, registry); // Every Page should have a corresponding RouteMatch. - assert(keyToPage.values.flattened - .every((Page page) => _routeMatchLookUp.containsKey(page))); + assert(keyToPage.values.flattened.every((Page page) => + pagePopContext.getRouteMatchForPage(page) != null)); return keyToPage[navigatorKey]!; } on _RouteBuilderError catch (e) { return >[ _buildErrorPage(context, e, matchList.uri), ]; } finally { - /// Clean up previous cache to prevent memory leak. + /// Clean up previous cache to prevent memory leak, making sure any nested + /// stateful shell routes for the current match list are kept. + final Set activeKeys = keyToPage.keys.toSet() + ..addAll(_nestedStatefulNavigatorKeys(matchList)); _goHeroCache.removeWhere( - (GlobalKey key, _) => !keyToPage.keys.contains(key)); + (GlobalKey key, _) => !activeKeys.contains(key)); } } + static Set> _nestedStatefulNavigatorKeys( + RouteMatchList matchList) { + final StatefulShellRoute? shellRoute = + matchList.routes.whereType().firstOrNull; + if (shellRoute == null) { + return >{}; + } + return RouteBase.routesRecursively([shellRoute]) + .whereType() + .expand((StatefulShellRoute e) => + e.branches.map((StatefulShellBranch b) => b.navigatorKey)) + .toSet(); + } + void _buildRecursive( BuildContext context, RouteMatchList matchList, int startIndex, - PopPageCallback onPopPage, + _PagePopContext pagePopContext, bool routerNeglect, Map, List>> keyToPages, GlobalKey navigatorKey, @@ -178,9 +207,9 @@ class RouteBuilder { final RouteBase route = match.route; final GoRouterState state = buildState(matchList, match); + Page? page; if (route is GoRoute) { - final Page page = _buildPageForRoute(context, state, match); - registry[page] = state; + page = _buildPageForGoRoute(context, state, match, route, pagePopContext); // If this GoRoute is for a different Navigator, add it to the // list of out of scope pages final GlobalKey goRouteNavKey = @@ -188,68 +217,100 @@ class RouteBuilder { keyToPages.putIfAbsent(goRouteNavKey, () => >[]).add(page); - _buildRecursive(context, matchList, startIndex + 1, onPopPage, + _buildRecursive(context, matchList, startIndex + 1, pagePopContext, routerNeglect, keyToPages, navigatorKey, registry); - } else if (route is ShellRoute) { + } else if (route is ShellRouteBase) { + assert(startIndex + 1 < matchList.matches.length, + 'Shell routes must always have child routes'); // The key for the Navigator that will display this ShellRoute's page. final GlobalKey parentNavigatorKey = navigatorKey; - // The key to provide to the ShellRoute's Navigator. - final GlobalKey shellNavigatorKey = route.navigatorKey; - - // The observers list for the ShellRoute's Navigator. - final List observers = - route.observers ?? []; - // Add an entry for the parent navigator if none exists. keyToPages.putIfAbsent(parentNavigatorKey, () => >[]); - // Add an entry for the shell route's navigator - keyToPages.putIfAbsent(shellNavigatorKey, () => >[]); - // Calling _buildRecursive can result in adding pages to the // parentNavigatorKey entry's list. Store the current length so // that the page for this ShellRoute is placed at the right index. final int shellPageIdx = keyToPages[parentNavigatorKey]!.length; + // Get the current sub-route of this shell route from the match list. + final RouteBase subRoute = matchList.matches[startIndex + 1].route; + + // The key to provide to the shell route's Navigator. + final GlobalKey shellNavigatorKey = + route.navigatorKeyForSubRoute(subRoute); + + // Add an entry for the shell route's navigator + keyToPages.putIfAbsent(shellNavigatorKey, () => >[]); + // Build the remaining pages - _buildRecursive(context, matchList, startIndex + 1, onPopPage, + _buildRecursive(context, matchList, startIndex + 1, pagePopContext, routerNeglect, keyToPages, shellNavigatorKey, registry); final HeroController heroController = _goHeroCache.putIfAbsent( shellNavigatorKey, () => _getHeroController(context)); - // Build the Navigator - final Widget child = HeroControllerScope( - controller: heroController, - child: _buildNavigator( - onPopPage, keyToPages[shellNavigatorKey]!, shellNavigatorKey, - observers: observers), + + // Build the Navigator for this shell route + Widget buildShellNavigator( + List? observers, String? restorationScopeId) { + return _buildNavigator( + pagePopContext.onPopPage, + keyToPages[shellNavigatorKey]!, + shellNavigatorKey, + observers: observers ?? const [], + restorationScopeId: restorationScopeId, + heroController: heroController, + ); + } + + // Call the ShellRouteBase to create/update the shell route state + final ShellRouteContext shellRouteContext = ShellRouteContext( + route: route, + routerState: state, + navigatorKey: shellNavigatorKey, + routeMatchList: matchList, + navigatorBuilder: buildShellNavigator, ); // Build the Page for this route - final Page page = - _buildPageForRoute(context, state, match, child: child); - registry[page] = state; + page = _buildPageForShellRoute( + context, state, match, route, pagePopContext, shellRouteContext); // Place the ShellRoute's Page onto the list for the parent navigator. keyToPages .putIfAbsent(parentNavigatorKey, () => >[]) .insert(shellPageIdx, page); } + if (page != null) { + registry[page] = state; + pagePopContext._setRouteMatchForPage(page, match); + } else { + throw _RouteBuilderException('Unsupported route type $route'); + } } - Navigator _buildNavigator( + static Widget _buildNavigator( PopPageCallback onPopPage, List> pages, Key? navigatorKey, { List observers = const [], + String? restorationScopeId, + HeroController? heroController, }) { - return Navigator( + final Widget navigator = Navigator( key: navigatorKey, restorationScopeId: restorationScopeId, pages: pages, observers: observers, onPopPage: onPopPage, ); + if (heroController != null) { + return HeroControllerScope( + controller: heroController, + child: navigator, + ); + } else { + return navigator; + } } /// Helper method that builds a [GoRouterState] object for the given [match] @@ -282,70 +343,72 @@ class RouteBuilder { ); } - /// Builds a [Page] for [StackedRoute] - Page _buildPageForRoute( - BuildContext context, GoRouterState state, RouteMatch match, - {Widget? child}) { - final RouteBase route = match.route; + /// Builds a [Page] for [GoRoute] + Page _buildPageForGoRoute(BuildContext context, GoRouterState state, + RouteMatch match, GoRoute route, _PagePopContext pagePopContext) { Page? page; - if (route is GoRoute) { - // Call the pageBuilder if it's non-null - final GoRouterPageBuilder? pageBuilder = route.pageBuilder; - if (pageBuilder != null) { - page = pageBuilder(context, state); - } - } else if (route is ShellRoute) { - final ShellRoutePageBuilder? pageBuilder = route.pageBuilder; - assert(child != null, 'ShellRoute must contain a child route'); - if (pageBuilder != null) { - page = pageBuilder(context, state, child!); + // Call the pageBuilder if it's non-null + final GoRouterPageBuilder? pageBuilder = route.pageBuilder; + if (pageBuilder != null) { + page = pageBuilder(context, state); + if (page is NoOpPage) { + page = null; } } - if (page is NoOpPage) { - page = null; - } - - page ??= buildPage(context, state, Builder(builder: (BuildContext context) { - return _callRouteBuilder(context, state, match, childWidget: child); - })); - _routeMatchLookUp[page] = match; - // Return the result of the route's builder() or pageBuilder() - return page; + return page ?? + buildPage(context, state, Builder(builder: (BuildContext context) { + return _callGoRouteBuilder(context, state, route); + })); } - /// Calls the user-provided route builder from the [RouteMatch]'s [RouteBase]. - Widget _callRouteBuilder( - BuildContext context, GoRouterState state, RouteMatch match, - {Widget? childWidget}) { - final RouteBase route = match.route; - - if (route is GoRoute) { - final GoRouterWidgetBuilder? builder = route.builder; + /// Calls the user-provided route builder from the [GoRoute]. + Widget _callGoRouteBuilder( + BuildContext context, GoRouterState state, GoRoute route) { + final GoRouterWidgetBuilder? builder = route.builder; - if (builder == null) { - throw _RouteBuilderError('No routeBuilder provided to GoRoute: $route'); - } + if (builder == null) { + throw _RouteBuilderError('No routeBuilder provided to GoRoute: $route'); + } - return builder(context, state); - } else if (route is ShellRoute) { - if (childWidget == null) { - throw _RouteBuilderException( - 'Attempt to build ShellRoute without a child widget'); - } + return builder(context, state); + } - final ShellRouteBuilder? builder = route.builder; + /// Builds a [Page] for [ShellRouteBase] + Page _buildPageForShellRoute( + BuildContext context, + GoRouterState state, + RouteMatch match, + ShellRouteBase route, + _PagePopContext pagePopContext, + ShellRouteContext shellRouteContext) { + Page? page = route.buildPage(context, state, shellRouteContext); + if (page is NoOpPage) { + page = null; + } - if (builder == null) { - throw _RouteBuilderError('No builder provided to ShellRoute: $route'); - } + // Return the result of the route's builder() or pageBuilder() + return page ?? + buildPage(context, state, Builder(builder: (BuildContext context) { + return _callShellRouteBaseBuilder( + context, state, route, shellRouteContext); + })); + } - return builder(context, state, childWidget); + /// Calls the user-provided route builder from the [ShellRouteBase]. + Widget _callShellRouteBaseBuilder(BuildContext context, GoRouterState state, + ShellRouteBase route, ShellRouteContext? shellRouteContext) { + assert(shellRouteContext != null, + 'ShellRouteContext must be provided for ${route.runtimeType}'); + final Widget? widget = + route.buildWidget(context, state, shellRouteContext!); + if (widget == null) { + throw _RouteBuilderError('No builder provided to ShellRoute: $route'); } - throw _RouteBuilderException('Unsupported route type $route'); + return widget; } _PageBuilderForAppType? _pageBuilderForAppType; @@ -427,10 +490,10 @@ class RouteBuilder { BuildContext context, _RouteBuilderError e, Uri uri, - PopPageCallback onPopPage, + PopPageWithRouteMatchCallback onPopPage, GlobalKey navigatorKey) { return _buildNavigator( - onPopPage, + (Route route, dynamic result) => onPopPage(route, result, null), >[ _buildErrorPage(context, e, uri), ], @@ -529,3 +592,33 @@ class _RouteBuilderException implements Exception { return '$message ${exception ?? ""}'; } } + +/// Context used to provide a route to page association when popping routes. +class _PagePopContext { + _PagePopContext._(this.onPopPageWithRouteMatch); + + final Map, RouteMatch> _routeMatchLookUp = + , RouteMatch>{}; + + /// On pop page callback that includes the associated [RouteMatch]. + final PopPageWithRouteMatchCallback onPopPageWithRouteMatch; + + /// Looks for the [RouteMatch] for a given [Page]. + /// + /// The [Page] must have been previously built via the [RouteBuilder] that + /// created this [PagePopContext]; otherwise, this method returns null. + RouteMatch? getRouteMatchForPage(Page page) => + _routeMatchLookUp[page]; + + void _setRouteMatchForPage(Page page, RouteMatch match) => + _routeMatchLookUp[page] = match; + + /// Function used as [Navigator.onPopPage] callback when creating Navigators. + /// + /// This function forwards to [onPopPageWithRouteMatch], including the + /// [RouteMatch] associated with the popped route. + bool onPopPage(Route route, dynamic result) { + final Page page = route.settings as Page; + return onPopPageWithRouteMatch(route, result, _routeMatchLookUp[page]); + } +} diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 2c2f8e77678a..07d02d469c3c 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart'; import 'configuration.dart'; import 'logging.dart'; +import 'matching.dart'; import 'misc/errors.dart'; import 'path_utils.dart'; import 'typedefs.dart'; @@ -25,6 +26,8 @@ class RouteConfiguration { _debugVerifyNoDuplicatePathParameter(routes, {})), assert(_debugCheckParentNavigatorKeys( routes, >[navigatorKey])) { + assert(_debugCheckStatefulShellBranchDefaultLocations( + routes, RouteMatcher(this))); _cacheNameToPath('', routes); log.info(debugKnownRoutes()); } @@ -41,7 +44,7 @@ class RouteConfiguration { 'sub-route path may not start or end with /: $route'); } subRouteIsTopLevel = false; - } else if (route is ShellRoute) { + } else if (route is ShellRouteBase) { subRouteIsTopLevel = isTopLevel; } _debugCheckPath(route.routes, subRouteIsTopLevel); @@ -86,6 +89,21 @@ class RouteConfiguration { route.routes, >[...allowedKeys..add(route.navigatorKey)], ); + } else if (route is StatefulShellRoute) { + for (final StatefulShellBranch branch in route.branches) { + assert( + !allowedKeys.contains(branch.navigatorKey), + 'StatefulShellBranch must not reuse an ancestor navigatorKey ' + '(${branch.navigatorKey})'); + + _debugCheckParentNavigatorKeys( + branch.routes, + >[ + ...allowedKeys, + branch.navigatorKey, + ], + ); + } } } return true; @@ -111,6 +129,56 @@ class RouteConfiguration { return true; } + // Check to see that the configured initialLocation of StatefulShellBranches + // points to a descendant route of the route branch. + bool _debugCheckStatefulShellBranchDefaultLocations( + List routes, RouteMatcher matcher) { + try { + for (final RouteBase route in routes) { + if (route is StatefulShellRoute) { + for (final StatefulShellBranch branch in route.branches) { + if (branch.initialLocation == null) { + // Recursively search for the first GoRoute descendant. Will + // throw assertion error if not found. + final GoRoute? route = branch.defaultRoute; + final String? initialLocation = + route != null ? locationForRoute(route) : null; + assert( + initialLocation != null, + 'The default location of a StatefulShellBranch must be ' + 'derivable from GoRoute descendant'); + assert( + route!.pathParameters.isEmpty, + 'The default location of a StatefulShellBranch cannot be ' + 'a parameterized route'); + } else { + final List matchRoutes = + matcher.findMatch(branch.initialLocation!).routes; + final int shellIndex = matchRoutes.indexOf(route); + bool matchFound = false; + if (shellIndex >= 0 && (shellIndex + 1) < matchRoutes.length) { + final RouteBase branchRoot = matchRoutes[shellIndex + 1]; + matchFound = branch.routes.contains(branchRoot); + } + assert( + matchFound, + 'The initialLocation (${branch.initialLocation}) of ' + 'StatefulShellBranch must match a descendant route of the ' + 'branch'); + } + } + } + _debugCheckStatefulShellBranchDefaultLocations(route.routes, matcher); + } + } on MatcherError catch (e) { + assert( + false, + 'initialLocation (${e.location}) of StatefulShellBranch must ' + 'be a valid location'); + } + return true; + } + /// The list of top level routes used by [GoRouterDelegate]. final List routes; @@ -167,6 +235,13 @@ class RouteConfiguration { .toString(); } + /// Get the location for the provided route. + /// + /// Builds the absolute path for the route, by concatenating the paths of the + /// route and all its ancestors. + String? locationForRoute(RouteBase route) => + fullPathForRoute(route, '', routes); + @override String toString() { return 'RouterConfiguration: $routes'; @@ -222,7 +297,7 @@ class RouteConfiguration { if (route.routes.isNotEmpty) { _cacheNameToPath(fullPath, route.routes); } - } else if (route is ShellRoute) { + } else if (route is ShellRouteBase) { if (route.routes.isNotEmpty) { _cacheNameToPath(parentFullPath, route.routes); } diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index a62906adf1bf..985dce389d4d 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -27,25 +27,29 @@ class GoRouterDelegate extends RouterDelegate required List observers, required this.routerNeglect, String? restorationScopeId, - }) : _configuration = configuration, - builder = RouteBuilder( - configuration: configuration, - builderWithNav: builderWithNav, - errorPageBuilder: errorPageBuilder, - errorBuilder: errorBuilder, - restorationScopeId: restorationScopeId, - observers: observers, - ); + }) : _configuration = configuration { + builder = RouteBuilder( + configuration: configuration, + builderWithNav: builderWithNav, + errorPageBuilder: errorPageBuilder, + errorBuilder: errorBuilder, + restorationScopeId: restorationScopeId, + observers: observers, + onPopPageWithRouteMatch: _handlePopPageWithRouteMatch, + ); + } /// Builds the top-level Navigator given a configuration and location. @visibleForTesting - final RouteBuilder builder; + late final RouteBuilder builder; /// Set to true to disable creating history entries on the web. final bool routerNeglect; RouteMatchList _matchList = RouteMatchList.empty; + final RouteConfiguration _configuration; + /// Stores the number of times each route route has been pushed. /// /// This is used to generate a unique key for each route. @@ -58,7 +62,6 @@ class GoRouterDelegate extends RouterDelegate /// } /// ``` final Map _pushCounts = {}; - final RouteConfiguration _configuration; _NavigatorStateIterator _createNavigatorStateIterator() => _NavigatorStateIterator(_matchList, navigatorKey.currentState!); @@ -85,10 +88,6 @@ class GoRouterDelegate extends RouterDelegate Future _push( RouteMatchList matches, ValueKey pageKey) async { final ImperativeRouteMatch newPageKeyMatch = ImperativeRouteMatch( - route: matches.last.route, - matchedLocation: matches.last.matchedLocation, - extra: matches.last.extra, - error: matches.last.error, pageKey: pageKey, matches: matches, ); @@ -149,12 +148,11 @@ class GoRouterDelegate extends RouterDelegate ); } - bool _onPopPage(Route route, Object? result) { + bool _handlePopPageWithRouteMatch( + Route route, Object? result, RouteMatch? match) { if (!route.didPop(result)) { return false; } - final Page page = route.settings as Page; - final RouteMatch? match = builder.getRouteMatchForPage(page); assert(match != null); if (match is ImperativeRouteMatch) { match.complete(result); @@ -219,7 +217,6 @@ class GoRouterDelegate extends RouterDelegate return builder.build( context, _matchList, - _onPopPage, routerNeglect, ); } @@ -257,6 +254,7 @@ class _NavigatorStateIterator extends Iterator { if (index < 0) { return false; } + late RouteBase subRoute; for (index -= 1; index >= 0; index -= 1) { final RouteMatch match = matchList.matches[index]; final RouteBase route = match.route; @@ -294,19 +292,22 @@ class _NavigatorStateIterator extends Iterator { current = parentNavigatorKey.currentState!; return true; - } else if (route is ShellRoute) { + } else if (route is ShellRouteBase) { // Must have a ModalRoute parent because the navigator ShellRoute // created must not be the root navigator. + final GlobalKey navigatorKey = + route.navigatorKeyForSubRoute(subRoute); final ModalRoute parentModalRoute = - ModalRoute.of(route.navigatorKey.currentContext!)!; + ModalRoute.of(navigatorKey.currentContext!)!; // There may be pageless route on top of ModalRoute that the // parentNavigatorKey is in. For example an open dialog. if (parentModalRoute.isCurrent == false) { continue; } - current = route.navigatorKey.currentState!; + current = navigatorKey.currentState!; return true; } + subRoute = route; } assert(index == -1); current = root; diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index 7c3c736bbeb1..24aa0587bc89 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -36,7 +36,7 @@ class RouteMatch { required Map pathParameters, required Object? extra, }) { - if (route is ShellRoute) { + if (route is ShellRouteBase) { return RouteMatch( route: route, matchedLocation: remainingLocation, @@ -112,13 +112,15 @@ class RouteMatch { 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(); + }) : _completer = Completer(), + super( + route: matches.last.route, + matchedLocation: matches.last.matchedLocation, + extra: matches.last.extra, + error: matches.last.error, + ); /// The matches that produces this route match. final RouteMatchList matches; diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index 5855204bd467..b809ccae5c06 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -158,7 +158,7 @@ class RouteMatchList { newMatches.removeRange(index, newMatches.length); // Also pop ShellRoutes when there are no subsequent route matches - while (newMatches.isNotEmpty && newMatches.last.route is ShellRoute) { + while (newMatches.isNotEmpty && newMatches.last.route is ShellRouteBase) { newMatches.removeLast(); } // Removing ImperativeRouteMatch should not change uri and pathParameters. @@ -193,10 +193,13 @@ class RouteMatchList { 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; + bool get isError => error != null; /// Returns the error that this match intends to display. - Exception? get error => matches.first.error; + Exception? get error => matches.firstOrNull?.error; + + /// The routes for each of the matches. + List get routes => matches.map((RouteMatch e) => e.route).toList(); RouteMatchList _copyWith({ List? matches, @@ -237,6 +240,115 @@ class RouteMatchList { String toString() { return '${objectRuntimeType(this, 'RouteMatchList')}($fullPath)'; } + + /// Returns a pre-parsed [RouteInformation], containing a reference to this + /// match list. + RouteInformation toPreParsedRouteInformation() { + return RouteInformation( + // TODO(tolo): remove this ignore and migrate the code + // https://github.com/flutter/flutter/issues/124045. + // ignore: deprecated_member_use + location: uri.toString(), + state: this, + ); + } + + /// Attempts to extract a pre-parsed match list from the provided + /// [RouteInformation]. + static RouteMatchList? fromPreParsedRouteInformation( + RouteInformation routeInformation) { + if (routeInformation.state is RouteMatchList) { + return routeInformation.state! as RouteMatchList; + } + return null; + } +} + +/// Handles encoding and decoding of [RouteMatchList] objects to a format +/// suitable for using with [StandardMessageCodec]. +/// +/// The primary use of this class is for state restoration. +class RouteMatchListCodec { + /// Creates a new [RouteMatchListCodec] object. + RouteMatchListCodec(this._matcher); + + static const String _encodedDataKey = 'go_router/encoded_route_match_list'; + static const String _locationKey = 'location'; + static const String _stateKey = 'state'; + static const String _imperativeMatchesKey = 'imperativeMatches'; + static const String _pageKey = 'pageKey'; + + final RouteMatcher _matcher; + + /// Encodes the provided [RouteMatchList]. + Object? encodeMatchList(RouteMatchList matchlist) { + if (matchlist.isEmpty) { + return null; + } + final List> imperativeMatches = matchlist.matches + .whereType>() + .map((ImperativeRouteMatch e) => _toPrimitives( + e.matches.uri.toString(), e.extra, + pageKey: e.pageKey.value)) + .toList(); + + return { + _encodedDataKey: _toPrimitives( + matchlist.uri.toString(), matchlist.matches.first.extra, + imperativeMatches: imperativeMatches), + }; + } + + static Map _toPrimitives(String location, Object? state, + {List? imperativeMatches, String? pageKey}) { + return { + _locationKey: location, + _stateKey: state, + if (imperativeMatches != null) _imperativeMatchesKey: imperativeMatches, + if (pageKey != null) _pageKey: pageKey, + }; + } + + /// Attempts to decode the provided object into a [RouteMatchList]. + RouteMatchList? decodeMatchList(Object? object) { + if (object is Map && object[_encodedDataKey] is Map) { + final Map data = + object[_encodedDataKey] as Map; + final Object? rootLocation = data[_locationKey]; + if (rootLocation is! String) { + return null; + } + final RouteMatchList matchList = + _matcher.findMatch(rootLocation, extra: data[_stateKey]); + + final List? imperativeMatches = + data[_imperativeMatchesKey] as List?; + if (imperativeMatches != null) { + for (int i = 0; i < imperativeMatches.length; i++) { + final Object? match = imperativeMatches[i]; + if (match is! Map || + match[_locationKey] is! String || + match[_pageKey] is! String) { + continue; + } + final ValueKey pageKey = + ValueKey(match[_pageKey] as String); + final RouteMatchList imperativeMatchList = _matcher.findMatch( + match[_locationKey] as String, + extra: match[_stateKey]); + final ImperativeRouteMatch imperativeMatch = + ImperativeRouteMatch( + pageKey: pageKey, + matches: imperativeMatchList, + ); + matchList.push(imperativeMatch); + } + } + + return matchList; + } + return null; + } } /// An error that occurred during matching. @@ -307,7 +419,7 @@ List? _getLocRouteRecursively({ // Otherwise, recurse final String childRestLoc; final String newParentSubLoc; - if (match.route is ShellRoute) { + if (match.route is ShellRouteBase) { childRestLoc = remainingLocation; newParentSubLoc = matchedLocation; } else { diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index 05dbd0f3174f..c4ff4ad6a95a 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -130,5 +130,8 @@ extension GoRouterHelper on BuildContext { Map queryParameters = const {}, Object? extra, }) => - GoRouter.of(this).replaceNamed(name, extra: extra); + GoRouter.of(this).replaceNamed(name, + pathParameters: pathParameters, + queryParameters: queryParameters, + extra: extra); } diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index 7fd00cfea390..78a7ee0624ec 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -57,11 +57,17 @@ class GoRouteInformationParser extends RouteInformationParser { ) { late final RouteMatchList initialMatches; try { - // TODO(chunhtai): remove this ignore and migrate the code - // https://github.com/flutter/flutter/issues/124045. - // ignore: deprecated_member_use, unnecessary_non_null_assertion - initialMatches = matcher.findMatch(routeInformation.location!, - extra: routeInformation.state); + final RouteMatchList? preParsedMatchList = + RouteMatchList.fromPreParsedRouteInformation(routeInformation); + if (preParsedMatchList != null) { + initialMatches = preParsedMatchList; + } else { + // TODO(chunhtai): remove this ignore and migrate the code + // https://github.com/flutter/flutter/issues/124045. + // ignore: deprecated_member_use, unnecessary_non_null_assertion + initialMatches = matcher.findMatch(routeInformation.location!, + extra: routeInformation.state); + } } on MatcherError { // TODO(chunhtai): remove this ignore and migrate the code // https://github.com/flutter/flutter/issues/124045. diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart index e9db923d71c5..ba403f478334 100644 --- a/packages/go_router/lib/src/path_utils.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import '../go_router.dart'; + final RegExp _parameterRegExp = RegExp(r':(\w+)(\((?:\\.|[^\\()])+\))?'); /// Converts a [pattern] such as `/user/:id` into [RegExp]. @@ -135,3 +137,24 @@ String canonicalUri(String loc) { return canon; } + +/// Builds an absolute path for the provided route. +String? fullPathForRoute( + RouteBase targetRoute, String parentFullpath, List routes) { + for (final RouteBase route in routes) { + final String fullPath = (route is GoRoute) + ? concatenatePaths(parentFullpath, route.path) + : parentFullpath; + + if (route == targetRoute) { + return fullPath; + } else { + final String? subRoutePath = + fullPathForRoute(targetRoute, fullPath, route.routes); + if (subRoutePath != null) { + return subRoutePath; + } + } + } + return null; +} diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 21e805364916..80a885ed8afd 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -2,11 +2,13 @@ // 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/widgets.dart'; import 'package:meta/meta.dart'; -import 'configuration.dart'; -import 'pages/custom_transition_page.dart'; +import '../go_router.dart'; +import 'match.dart'; +import 'matching.dart'; import 'path_utils.dart'; import 'typedefs.dart'; @@ -101,6 +103,13 @@ abstract class RouteBase { /// The list of child routes associated with this route. final List routes; + + /// Builds a lists containing the provided routes along with all their + /// descendant [routes]. + static Iterable routesRecursively(Iterable routes) { + return routes.expand( + (RouteBase e) => [e, ...routesRecursively(e.routes)]); + } } /// A route that is displayed visually above the matching parent route using the @@ -320,6 +329,72 @@ class GoRoute extends RouteBase { late final RegExp _pathRE; } +/// Base class for classes that act as shells for sub-routes, such +/// as [ShellRoute] and [StatefulShellRoute]. +abstract class ShellRouteBase extends RouteBase { + /// Constructs a [ShellRouteBase]. + const ShellRouteBase._({super.routes}) : super._(); + + /// Attempts to build the Widget representing this shell route. + /// + /// Returns null if this shell route does not build a Widget, but instead uses + /// a Page to represent itself (see [buildPage]). + Widget? buildWidget(BuildContext context, GoRouterState state, + ShellRouteContext shellRouteContext); + + /// Attempts to build the Page representing this shell route. + /// + /// Returns null if this shell route does not build a Page, but instead uses + /// a Widget to represent itself (see [buildWidget]). + Page? buildPage(BuildContext context, GoRouterState state, + ShellRouteContext shellRouteContext); + + /// Returns the key for the [Navigator] that is to be used for the specified + /// immediate sub-route of this shell route. + GlobalKey navigatorKeyForSubRoute(RouteBase subRoute); + + /// Returns the keys for the [Navigator]s associated with this shell route. + Iterable> get _navigatorKeys => + >[]; + + /// Returns all the Navigator keys of this shell route as well as those of any + /// descendant shell routes. + Iterable> _navigatorKeysRecursively() { + return RouteBase.routesRecursively([this]) + .whereType() + .expand((ShellRouteBase e) => e._navigatorKeys); + } +} + +/// Context object used when building the shell and Navigator for a shell route. +class ShellRouteContext { + /// Constructs a [ShellRouteContext]. + ShellRouteContext({ + required this.route, + required this.routerState, + required this.navigatorKey, + required this.routeMatchList, + required this.navigatorBuilder, + }); + + /// The associated shell route. + final ShellRouteBase route; + + /// The current route state associated with [route]. + final GoRouterState routerState; + + /// The [Navigator] key to be used for the nested navigation associated with + /// [route]. + final GlobalKey navigatorKey; + + /// The route match list representing the current location within the + /// associated shell route. + final RouteMatchList routeMatchList; + + /// Function used to build the [Navigator] for the current route. + final NavigatorBuilder navigatorBuilder; +} + /// A route that displays a UI shell around the matching child route. /// /// When a ShellRoute is added to the list of routes on GoRouter or GoRoute, a @@ -415,7 +490,7 @@ class GoRoute extends RouteBase { /// ``` /// /// {@category Configuration} -class ShellRoute extends RouteBase { +class ShellRoute extends ShellRouteBase { /// Constructs a [ShellRoute]. ShellRoute({ this.builder, @@ -423,6 +498,7 @@ class ShellRoute extends RouteBase { this.observers, super.routes, GlobalKey? navigatorKey, + this.restorationScopeId, }) : assert(routes.isNotEmpty), navigatorKey = navigatorKey ?? GlobalKey(), super._() { @@ -436,18 +512,42 @@ class ShellRoute extends RouteBase { /// The widget builder for a shell route. /// - /// Similar to GoRoute builder, but with an additional child parameter. This - /// child parameter is the Widget built by calling the matching sub-route's - /// builder. + /// Similar to [GoRoute.builder], but with an additional child parameter. This + /// child parameter is the Widget managing the nested navigation for the + /// matching sub-routes. Typically, a shell route builds its shell around this + /// Widget. final ShellRouteBuilder? builder; /// The page builder for a shell route. /// - /// Similar to GoRoute pageBuilder, but with an additional child parameter. - /// This child parameter is the Widget built by calling the matching - /// sub-route's builder. + /// Similar to [GoRoute.pageBuilder], but with an additional child parameter. + /// This child parameter is the Widget managing the nested navigation for the + /// matching sub-routes. Typically, a shell route builds its shell around this + /// Widget. final ShellRoutePageBuilder? pageBuilder; + @override + Widget? buildWidget(BuildContext context, GoRouterState state, + ShellRouteContext shellRouteContext) { + if (builder != null) { + final Widget navigator = + shellRouteContext.navigatorBuilder(observers, restorationScopeId); + return builder!(context, state, navigator); + } + return null; + } + + @override + Page? buildPage(BuildContext context, GoRouterState state, + ShellRouteContext shellRouteContext) { + if (pageBuilder != null) { + final Widget navigator = + shellRouteContext.navigatorBuilder(observers, restorationScopeId); + return pageBuilder!(context, state, navigator); + } + return null; + } + /// The observers for a shell route. /// /// The observers parameter is used by the [Navigator] built for this route. @@ -458,4 +558,720 @@ class ShellRoute extends RouteBase { /// All ShellRoutes build a Navigator by default. Child GoRoutes /// are placed onto this Navigator instead of the root Navigator. final GlobalKey navigatorKey; + + /// Restoration ID to save and restore the state of the navigator, including + /// its history. + final String? restorationScopeId; + + @override + GlobalKey navigatorKeyForSubRoute(RouteBase subRoute) { + assert(routes.contains(subRoute)); + return navigatorKey; + } + + @override + Iterable> get _navigatorKeys => + >[navigatorKey]; +} + +/// A route that displays a UI shell with separate [Navigator]s for its +/// sub-routes. +/// +/// Similar to [ShellRoute], this route class places its sub-route on a +/// different Navigator than the root [Navigator]. However, this route class +/// differs in that it creates separate [Navigator]s for each of its nested +/// branches (i.e. parallel navigation trees), making it possible to build an +/// app with stateful nested navigation. This is convenient when for instance +/// implementing a UI with a [BottomNavigationBar], with a persistent navigation +/// state for each tab. +/// +/// A StatefulShellRoute is created by specifying a List of +/// [StatefulShellBranch] items, each representing a separate stateful branch +/// in the route tree. StatefulShellBranch provides the root routes and the +/// Navigator key ([GlobalKey]) for the branch, as well as an optional initial +/// location. +/// +/// Like [ShellRoute], either a [builder] or a [pageBuilder] must be provided +/// when creating a StatefulShellRoute. However, these builders differ slightly +/// in that they accept a [StatefulNavigationShell] parameter instead of a +/// child Widget. The StatefulNavigationShell can be used to access information +/// about the state of the route, as well as to switch the active branch (i.e. +/// restoring the navigation stack of another branch). The latter is +/// accomplished by using the method [StatefulNavigationShell.goBranch], for +/// example: +/// +/// ``` +/// void _onItemTapped(int index) { +/// navigationShell.goBranch(index: index); +/// } +/// ``` +/// +/// The StatefulNavigationShell is also responsible for managing and maintaining +/// the state of the branch Navigators. Typically, a shell is built around this +/// Widget, for example by using it as the body of [Scaffold] with a +/// [BottomNavigationBar]. +/// +/// When creating a StatefulShellRoute, a [navigatorContainerBuilder] function +/// must be provided. This function is responsible for building the actual +/// container for the Widgets representing the branch Navigators. Typically, +/// the Widget returned by this function handles the layout (including +/// [Offstage] handling etc) of the branch Navigators and any animations needed +/// when switching active branch. +/// +/// For a default implementation of [navigatorContainerBuilder] that is +/// appropriate for most use cases, consider using the constructor +/// [StatefulShellRoute.indexedStack]. +/// +/// With StatefulShellRoute (and any route below it), animated transitions +/// between routes in the same navigation stack works the same way as with other +/// route classes, and can be customized using pageBuilder. However, since +/// StatefulShellRoute maintains a set of parallel navigation stacks, +/// any transitions when switching between branches is the responsibility of the +/// branch Navigator container (i.e. [navigatorContainerBuilder]). The default +/// [IndexedStack] implementation ([StatefulShellRoute.indexedStack]) does not +/// use animated transitions, but an example is provided on how to accomplish +/// this (see link to custom StatefulShellRoute example below). +/// +/// See also: +/// * [StatefulShellRoute.indexedStack] which provides a default +/// StatefulShellRoute implementation suitable for most use cases. +/// * [Stateful Nested Navigation example](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart) +/// for a complete runnable example using StatefulShellRoute. +/// * [Custom StatefulShellRoute example](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/others/custom_stateful_shell_route.dart) +/// which demonstrates how to customize the container for the branch Navigators +/// and how to implement animated transitions when switching branches. +class StatefulShellRoute extends ShellRouteBase { + /// Constructs a [StatefulShellRoute] from a list of [StatefulShellBranch]es, + /// each representing a separate nested navigation tree (branch). + /// + /// A separate [Navigator] will be created for each of the branches, using + /// the navigator key specified in [StatefulShellBranch]. The Widget + /// implementing the container for the branch Navigators is provided by + /// [navigatorContainerBuilder]. + StatefulShellRoute({ + required this.branches, + this.builder, + this.pageBuilder, + required this.navigatorContainerBuilder, + this.restorationScopeId, + }) : assert(branches.isNotEmpty), + assert((pageBuilder != null) ^ (builder != null), + 'One of builder or pageBuilder must be provided, but not both'), + assert(_debugUniqueNavigatorKeys(branches).length == branches.length, + 'Navigator keys must be unique'), + assert(_debugValidateParentNavigatorKeys(branches)), + assert(_debugValidateRestorationScopeIds(restorationScopeId, branches)), + super._(routes: _routes(branches)); + + /// Constructs a StatefulShellRoute that uses an [IndexedStack] for its + /// nested [Navigator]s. + /// + /// This constructor provides an IndexedStack based implementation for the + /// container ([navigatorContainerBuilder]) used to manage the Widgets + /// representing the branch Navigators. A part from that, this constructor + /// works the same way as the default constructor. + /// + /// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stacked_shell_route.dart) + /// for a complete runnable example using StatefulShellRoute.indexedStack. + StatefulShellRoute.indexedStack({ + required List branches, + StatefulShellRouteBuilder? builder, + StatefulShellRoutePageBuilder? pageBuilder, + String? restorationScopeId, + }) : this( + branches: branches, + builder: builder, + pageBuilder: pageBuilder, + restorationScopeId: restorationScopeId, + navigatorContainerBuilder: _indexedStackContainerBuilder, + ); + + /// Restoration ID to save and restore the state of the navigator, including + /// its history. + final String? restorationScopeId; + + /// The widget builder for a stateful shell route. + /// + /// Similar to [GoRoute.builder], but with an additional + /// [StatefulNavigationShell] parameter. StatefulNavigationShell is a Widget + /// responsible for managing the nested navigation for the + /// matching sub-routes. Typically, a shell route builds its shell around this + /// Widget. StatefulNavigationShell can also be used to access information + /// about which branch is active, and also to navigate to a different branch + /// (using [StatefulNavigationShell.goBranch]). + /// + /// Custom implementations may choose to ignore the child parameter passed to + /// the builder function, and instead use [StatefulNavigationShell] to + /// create a custom container for the branch Navigators. + final StatefulShellRouteBuilder? builder; + + /// The page builder for a stateful shell route. + /// + /// Similar to [GoRoute.pageBuilder], but with an additional + /// [StatefulNavigationShell] parameter. StatefulNavigationShell is a Widget + /// responsible for managing the nested navigation for the + /// matching sub-routes. Typically, a shell route builds its shell around this + /// Widget. StatefulNavigationShell can also be used to access information + /// about which branch is active, and also to navigate to a different branch + /// (using [StatefulNavigationShell.goBranch]). + /// + /// Custom implementations may choose to ignore the child parameter passed to + /// the builder function, and instead use [StatefulNavigationShell] to + /// create a custom container for the branch Navigators. + final StatefulShellRoutePageBuilder? pageBuilder; + + /// The builder for the branch Navigator container. + /// + /// The function responsible for building the container for the branch + /// Navigators. When this function is invoked, access is provided to a List of + /// Widgets representing the branch Navigators, where the the index + /// corresponds to the index of in [branches]. + /// + /// The builder function is expected to return a Widget that ensures that the + /// state of the branch Widgets is maintained, for instance by inducting them + /// in the Widget tree. + final ShellNavigationContainerBuilder navigatorContainerBuilder; + + /// Representations of the different stateful route branches that this + /// shell route will manage. + /// + /// Each branch uses a separate [Navigator], identified + /// [StatefulShellBranch.navigatorKey]. + final List branches; + + final GlobalKey _shellStateKey = + GlobalKey(); + + @override + Widget? buildWidget(BuildContext context, GoRouterState state, + ShellRouteContext shellRouteContext) { + if (builder != null) { + return builder!(context, state, _createShell(context, shellRouteContext)); + } + return null; + } + + @override + Page? buildPage(BuildContext context, GoRouterState state, + ShellRouteContext shellRouteContext) { + if (pageBuilder != null) { + return pageBuilder!( + context, state, _createShell(context, shellRouteContext)); + } + return null; + } + + @override + GlobalKey navigatorKeyForSubRoute(RouteBase subRoute) { + final StatefulShellBranch? branch = branches.firstWhereOrNull( + (StatefulShellBranch e) => e.routes.contains(subRoute)); + assert(branch != null); + return branch!.navigatorKey; + } + + @override + Iterable> get _navigatorKeys => + branches.map((StatefulShellBranch b) => b.navigatorKey); + + StatefulNavigationShell _createShell( + BuildContext context, ShellRouteContext shellRouteContext) => + StatefulNavigationShell( + shellRouteContext: shellRouteContext, + router: GoRouter.of(context), + containerBuilder: navigatorContainerBuilder); + + static Widget _indexedStackContainerBuilder(BuildContext context, + StatefulNavigationShell navigationShell, List children) { + return _IndexedStackedRouteBranchContainer( + currentIndex: navigationShell.currentIndex, children: children); + } + + static List _routes(List branches) => + branches.expand((StatefulShellBranch e) => e.routes).toList(); + + static Set> _debugUniqueNavigatorKeys( + List branches) => + Set>.from( + branches.map((StatefulShellBranch e) => e.navigatorKey)); + + static bool _debugValidateParentNavigatorKeys( + List branches) { + for (final StatefulShellBranch branch in branches) { + for (final RouteBase route in branch.routes) { + if (route is GoRoute) { + assert(route.parentNavigatorKey == null || + route.parentNavigatorKey == branch.navigatorKey); + } + } + } + return true; + } + + static bool _debugValidateRestorationScopeIds( + String? restorationScopeId, List branches) { + if (branches + .map((StatefulShellBranch e) => e.restorationScopeId) + .whereNotNull() + .isNotEmpty) { + assert( + restorationScopeId != null, + 'A restorationScopeId must be set for ' + 'the StatefulShellRoute when using restorationScopeIds on one or more ' + 'of the branches'); + } + return true; + } +} + +/// Representation of a separate branch in a stateful navigation tree, used to +/// configure [StatefulShellRoute]. +/// +/// The only required argument when creating a StatefulShellBranch is the +/// sub-routes ([routes]), however sometimes it may be convenient to also +/// provide a [initialLocation]. The value of this parameter is used when +/// loading the branch for the first time (for instance when switching branch +/// using the goBranch method in [StatefulNavigationShell]). +/// +/// A separate [Navigator] will be built for each StatefulShellBranch in a +/// [StatefulShellRoute], and the routes of this branch will be placed onto that +/// Navigator instead of the root Navigator. A custom [navigatorKey] can be +/// provided when creating a StatefulShellBranch, which can be useful when the +/// Navigator needs to be accessed elsewhere. If no key is provided, a default +/// one will be created. +@immutable +class StatefulShellBranch { + /// Constructs a [StatefulShellBranch]. + StatefulShellBranch({ + required this.routes, + GlobalKey? navigatorKey, + this.initialLocation, + this.restorationScopeId, + this.observers, + }) : navigatorKey = navigatorKey ?? GlobalKey(); + + /// The [GlobalKey] to be used by the [Navigator] built for this branch. + /// + /// A separate Navigator will be built for each StatefulShellBranch in a + /// [StatefulShellRoute] and this key will be used to identify the Navigator. + /// The routes associated with this branch will be placed o onto that + /// Navigator instead of the root Navigator. + final GlobalKey navigatorKey; + + /// The list of child routes associated with this route branch. + final List routes; + + /// The initial location for this route branch. + /// + /// If none is specified, the location of the first descendant [GoRoute] will + /// be used (i.e. [defaultRoute]). The initial location is used when loading + /// the branch for the first time (for instance when switching branch using + /// the goBranch method). + final String? initialLocation; + + /// Restoration ID to save and restore the state of the navigator, including + /// its history. + final String? restorationScopeId; + + /// The observers for this branch. + /// + /// The observers parameter is used by the [Navigator] built for this branch. + final List? observers; + + /// The default route of this branch, i.e. the first descendant [GoRoute]. + /// + /// This route will be used when loading the branch for the first time, if + /// an [initialLocation] has not been provided. + GoRoute? get defaultRoute => + RouteBase.routesRecursively(routes).whereType().firstOrNull; +} + +/// Builder for a custom container for the branch Navigators of a +/// [StatefulShellRoute]. +typedef ShellNavigationContainerBuilder = Widget Function(BuildContext context, + StatefulNavigationShell navigationShell, List children); + +/// Widget for managing the state of a [StatefulShellRoute]. +/// +/// Normally, this widget is not used directly, but is instead created +/// internally by StatefulShellRoute. However, if a custom container for the +/// branch Navigators is required, StatefulNavigationShell can be used in +/// the builder or pageBuilder methods of StatefulShellRoute to facilitate this. +/// The container is created using the provided [ShellNavigationContainerBuilder], +/// where the List of Widgets represent the Navigators for each branch. +/// +/// Example: +/// ``` +/// builder: (BuildContext context, GoRouterState state, +/// StatefulNavigationShell navigationShell) { +/// return StatefulNavigationShell( +/// shellRouteState: state, +/// containerBuilder: (_, __, List children) => MyCustomShell(shellState: state, children: children), +/// ); +/// } +/// ``` +class StatefulNavigationShell extends StatefulWidget { + /// Constructs an [StatefulNavigationShell]. + StatefulNavigationShell({ + required this.shellRouteContext, + required GoRouter router, + required this.containerBuilder, + }) : assert(shellRouteContext.route is StatefulShellRoute), + _router = router, + currentIndex = _indexOfBranchNavigatorKey( + shellRouteContext.route as StatefulShellRoute, + shellRouteContext.navigatorKey), + super( + key: + (shellRouteContext.route as StatefulShellRoute)._shellStateKey); + + /// The ShellRouteContext responsible for building the Navigator for the + /// current [StatefulShellBranch]. + final ShellRouteContext shellRouteContext; + + /// The builder for a custom container for shell route Navigators. + final ShellNavigationContainerBuilder containerBuilder; + + /// The index of the currently active [StatefulShellBranch]. + /// + /// Corresponds to the index in the branches field of [StatefulShellRoute]. + final int currentIndex; + + final GoRouter _router; + + /// The associated [StatefulShellRoute]. + StatefulShellRoute get route => shellRouteContext.route as StatefulShellRoute; + + /// Navigate to the last location of the [StatefulShellBranch] at the provided + /// index in the associated [StatefulShellBranch]. + /// + /// This method will switch the currently active branch [Navigator] for the + /// [StatefulShellRoute]. If the branch has not been visited before, or if + /// initialLocation is true, this method will navigate to initial location of + /// the branch (see [StatefulShellBranch.initialLocation]). + void goBranch(int index, {bool initialLocation = false}) { + final StatefulShellRoute route = + shellRouteContext.route as StatefulShellRoute; + final StatefulNavigationShellState? shellState = + route._shellStateKey.currentState; + if (shellState != null) { + shellState.goBranch(index, initialLocation: initialLocation); + } else { + _router.go(_effectiveInitialBranchLocation(index)); + } + } + + /// Gets the effective initial location for the branch at the provided index + /// in the associated [StatefulShellRoute]. + /// + /// The effective initial location is either the + /// [StackedShellBranch.initialLocation], if specified, or the location of the + /// [StackedShellBranch.defaultRoute]. + String _effectiveInitialBranchLocation(int index) { + final StatefulShellRoute route = + shellRouteContext.route as StatefulShellRoute; + final StatefulShellBranch branch = route.branches[index]; + final String? initialLocation = branch.initialLocation; + if (initialLocation != null) { + return initialLocation; + } else { + /// Recursively traverses the routes of the provided StackedShellBranch to + /// find the first GoRoute, from which a full path will be derived. + final GoRoute route = branch.defaultRoute!; + return _router.locationForRoute(route)!; + } + } + + @override + State createState() => StatefulNavigationShellState(); + + /// Gets the state for the nearest stateful shell route in the Widget tree. + static StatefulNavigationShellState of(BuildContext context) { + final StatefulNavigationShellState? shellState = + context.findAncestorStateOfType(); + assert(shellState != null); + return shellState!; + } + + /// Gets the state for the nearest stateful shell route in the Widget tree. + /// + /// Returns null if no stateful shell route is found. + static StatefulNavigationShellState? maybeOf(BuildContext context) { + final StatefulNavigationShellState? shellState = + context.findAncestorStateOfType(); + return shellState; + } + + static int _indexOfBranchNavigatorKey( + StatefulShellRoute route, GlobalKey navigatorKey) { + final int index = route.branches.indexWhere( + (StatefulShellBranch branch) => branch.navigatorKey == navigatorKey); + assert(index >= 0); + return index; + } +} + +/// State for StatefulNavigationShell. +class StatefulNavigationShellState extends State + with RestorationMixin { + final Map _branchNavigators = {}; + + /// The associated [StatefulShellRoute]. + StatefulShellRoute get route => widget.route; + + GoRouter get _router => widget._router; + RouteMatcher get _matcher => _router.routeInformationParser.matcher; + + final Map _branchLocations = + {}; + + @override + String? get restorationId => route.restorationScopeId; + + /// Generates a derived restoration ID for the branch location property, + /// falling back to the identity hash code of the branch to ensure an ID is + /// always returned (needed for _RestorableRouteMatchList/RestorableValue). + String _branchLocationRestorationScopeId(StatefulShellBranch branch) { + return branch.restorationScopeId != null + ? '${branch.restorationScopeId}-location' + : identityHashCode(branch).toString(); + } + + _RestorableRouteMatchList _branchLocation(StatefulShellBranch branch, + [bool register = true]) { + return _branchLocations.putIfAbsent(branch, () { + final _RestorableRouteMatchList branchLocation = + _RestorableRouteMatchList(_matcher); + if (register) { + registerForRestoration( + branchLocation, _branchLocationRestorationScopeId(branch)); + } + return branchLocation; + }); + } + + RouteMatchList? _matchListForBranch(int index) => + _branchLocations[route.branches[index]]?.value; + + /// Creates a new RouteMatchList that is scoped to the Navigators of the + /// current shell route or it's descendants. This involves removing all the + /// trailing imperative matches from the RouterMatchList that are targeted at + /// any other (often top-level) Navigator. + RouteMatchList _scopedMatchList(RouteMatchList matchList) { + final Iterable> validKeys = + route._navigatorKeysRecursively(); + final int index = matchList.matches.indexWhere((RouteMatch e) { + final RouteBase route = e.route; + if (e is ImperativeRouteMatch && route is GoRoute) { + return route.parentNavigatorKey != null && + !validKeys.contains(route.parentNavigatorKey); + } + return false; + }); + if (index > 0) { + final List matches = matchList.matches.sublist(0, index); + return RouteMatchList( + matches: matches, + uri: Uri.parse(matches.last.matchedLocation), + pathParameters: matchList.pathParameters, + ); + } + return matchList; + } + + void _updateCurrentBranchStateFromWidget() { + final StatefulShellBranch branch = route.branches[widget.currentIndex]; + final ShellRouteContext shellRouteContext = widget.shellRouteContext; + final RouteMatchList currentBranchLocation = + _scopedMatchList(shellRouteContext.routeMatchList); + + final _RestorableRouteMatchList branchLocation = + _branchLocation(branch, false); + final RouteMatchList previousBranchLocation = branchLocation.value; + branchLocation.value = currentBranchLocation; + final bool hasExistingNavigator = + _branchNavigators[branch.navigatorKey] != null; + + /// Only update the Navigator of the route match list has changed + final bool locationChanged = + previousBranchLocation != currentBranchLocation; + if (locationChanged || !hasExistingNavigator) { + _branchNavigators[branch.navigatorKey] = shellRouteContext + .navigatorBuilder(branch.observers, branch.restorationScopeId); + } + } + + /// The index of the currently active [StatefulShellBranch]. + /// + /// Corresponds to the index in the branches field of [StatefulShellRoute]. + int get currentIndex => widget.currentIndex; + + /// Navigate to the last location of the [StatefulShellBranch] at the provided + /// index in the associated [StatefulShellBranch]. + /// + /// This method will switch the currently active branch [Navigator] for the + /// [StatefulShellRoute]. If the branch has not been visited before, or if + /// initialLocation is true, this method will navigate to initial location of + /// the branch (see [StatefulShellBranch.initialLocation]). + void goBranch(int index, {bool initialLocation = false}) { + assert(index >= 0 && index < route.branches.length); + final RouteMatchList? matchlist = + initialLocation ? null : _matchListForBranch(index); + if (matchlist != null && matchlist.isNotEmpty) { + final RouteInformation preParsed = + matchlist.toPreParsedRouteInformation(); + // TODO(tolo): remove this ignore and migrate the code + // https://github.com/flutter/flutter/issues/124045. + // ignore: deprecated_member_use, unnecessary_non_null_assertion + _router.go(preParsed.location!, extra: preParsed.state); + } else { + _router.go(widget._effectiveInitialBranchLocation(index)); + } + } + + @override + void initState() { + super.initState(); + _updateCurrentBranchStateFromWidget(); + } + + @override + void dispose() { + super.dispose(); + for (final StatefulShellBranch branch in route.branches) { + _branchLocations[branch]?.dispose(); + } + } + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + route.branches.forEach(_branchLocation); + } + + @override + void didUpdateWidget(covariant StatefulNavigationShell oldWidget) { + super.didUpdateWidget(oldWidget); + _updateCurrentBranchStateFromWidget(); + } + + @override + Widget build(BuildContext context) { + final List children = route.branches + .map((StatefulShellBranch branch) => _BranchNavigatorProxy( + key: ObjectKey(branch), + branch: branch, + navigatorForBranch: (StatefulShellBranch b) => + _branchNavigators[b.navigatorKey])) + .toList(); + + return widget.containerBuilder(context, widget, children); + } +} + +/// [RestorableProperty] for enabling state restoration of [RouteMatchList]s. +class _RestorableRouteMatchList extends RestorableProperty { + _RestorableRouteMatchList(RouteMatcher matcher) + : _matchListCodec = RouteMatchListCodec(matcher); + + final RouteMatchListCodec _matchListCodec; + + RouteMatchList get value => _value; + RouteMatchList _value = RouteMatchList.empty; + set value(RouteMatchList newValue) { + if (newValue != _value) { + _value = newValue; + notifyListeners(); + } + } + + @override + void initWithValue(RouteMatchList value) { + _value = value; + } + + @override + RouteMatchList createDefaultValue() => RouteMatchList.empty; + + @override + RouteMatchList fromPrimitives(Object? data) { + return _matchListCodec.decodeMatchList(data) ?? RouteMatchList.empty; + } + + @override + Object? toPrimitives() { + if (value.isNotEmpty) { + return _matchListCodec.encodeMatchList(value); + } + return null; + } +} + +typedef _NavigatorForBranch = Widget? Function(StatefulShellBranch); + +/// Widget that serves as the proxy for a branch Navigator Widget, which +/// possibly hasn't been created yet. +/// +/// This Widget hides the logic handling whether a Navigator Widget has been +/// created yet for a branch or not, and at the same time ensures that the same +/// Widget class is consistently passed to the containerBuilder. The latter is +/// important for container implementations that cache child widgets, +/// such as [TabBarView]. +class _BranchNavigatorProxy extends StatefulWidget { + const _BranchNavigatorProxy({ + super.key, + required this.branch, + required this.navigatorForBranch, + }); + + final StatefulShellBranch branch; + final _NavigatorForBranch navigatorForBranch; + + @override + State createState() => _BranchNavigatorProxyState(); +} + +/// State for _BranchNavigatorProxy, using AutomaticKeepAliveClientMixin to +/// properly handle some scenarios where Slivers are used to manage the branches +/// (such as [TabBarView]). +class _BranchNavigatorProxyState extends State<_BranchNavigatorProxy> + with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + return widget.navigatorForBranch(widget.branch) ?? const SizedBox.shrink(); + } + + @override + bool get wantKeepAlive => true; +} + +/// Default implementation of a container widget for the [Navigator]s of the +/// route branches. This implementation uses an [IndexedStack] as a container. +class _IndexedStackedRouteBranchContainer extends StatelessWidget { + const _IndexedStackedRouteBranchContainer( + {required this.currentIndex, required this.children}); + + final int currentIndex; + + final List children; + + @override + Widget build(BuildContext context) { + final List stackItems = children + .mapIndexed((int index, Widget child) => + _buildRouteBranchContainer(context, currentIndex == index, child)) + .toList(); + + return IndexedStack(index: currentIndex, children: stackItems); + } + + Widget _buildRouteBranchContainer( + BuildContext context, bool isActive, Widget child) { + return Offstage( + offstage: !isActive, + child: TickerMode( + enabled: isActive, + child: child, + ), + ); + } } diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index e9b4ec64ec51..0e2d59b647a3 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -143,11 +143,6 @@ class GoRouter extends ChangeNotifier implements RouterConfig { GoRouteInformationParser get routeInformationParser => _routeInformationParser; - /// The route configuration. Used for testing. - // TODO(johnpryan): Remove this, integration tests shouldn't need access - @visibleForTesting - RouteConfiguration get routeConfiguration => _routeConfiguration; - /// Gets the current location. // TODO(chunhtai): deprecates this once go_router_builder is migrated to // GoRouterState.of. @@ -189,6 +184,13 @@ class GoRouter extends ChangeNotifier implements RouterConfig { queryParameters: queryParameters, ); + /// Get the location for the provided route. + /// + /// Builds the absolute path for the route, by concatenating the paths of the + /// route and all its ancestors. + String? locationForRoute(RouteBase route) => + _routeInformationParser.configuration.locationForRoute(route); + /// Navigate to a URI location w/ optional query parameters, e.g. /// `/family/f2/person/p1?color=blue` void go(String location, {Object? extra}) { diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart index 29269945b419..7a4071529f03 100644 --- a/packages/go_router/lib/src/typedefs.dart +++ b/packages/go_router/lib/src/typedefs.dart @@ -34,11 +34,18 @@ typedef ShellRoutePageBuilder = Page Function( Widget child, ); -/// The signature of the navigatorBuilder callback. -typedef GoRouterNavigatorBuilder = Widget Function( +/// The widget builder for [StatefulShellRoute]. +typedef StatefulShellRouteBuilder = Widget Function( BuildContext context, GoRouterState state, - Widget child, + StatefulNavigationShell navigationShell, +); + +/// The page builder for [StatefulShellRoute]. +typedef StatefulShellRoutePageBuilder = Page Function( + BuildContext context, + GoRouterState state, + StatefulNavigationShell navigationShell, ); /// Signature of a go router builder function with navigator. @@ -50,3 +57,7 @@ typedef GoRouterBuilderWithNav = Widget Function( /// The signature of the redirect callback. typedef GoRouterRedirect = FutureOr Function( BuildContext context, GoRouterState state); + +/// Signature for functions used to build Navigators +typedef NavigatorBuilder = Widget Function( + List? observers, String? restorationScopeId); diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 4d1cb1fda841..7aae5cb12e5e 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: 7.0.0 +version: 7.1.1 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,4 +21,5 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + io: ^1.0.4 path: ^1.8.2 diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 81f4182583f5..ac97cab5dd75 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -8,6 +8,9 @@ import 'package:go_router/src/builder.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'; + +import 'test_helpers.dart'; void main() { group('RouteBuilder', () { @@ -76,17 +79,13 @@ void main() { ); final RouteMatchList matches = RouteMatchList( - matches: [ - RouteMatch( - route: config.routes.first, - matchedLocation: '/', - extra: null, - error: null, - pageKey: const ValueKey('/'), - ), - ], - uri: Uri.parse('/'), - pathParameters: const {}); + matches: [ + createRouteMatch(config.routes.first, '/'), + createRouteMatch(config.routes.first.routes.first, '/'), + ], + uri: Uri.parse('/'), + pathParameters: const {}, + ); await tester.pumpWidget( _BuilderTestWidget( @@ -272,6 +271,104 @@ void main() { expect(find.byType(_HomeScreen), findsNothing); expect(find.byType(_DetailsScreen), findsOneWidget); }); + + testWidgets('Uses the correct restorationScopeId for ShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(debugLabel: 'root'); + final GlobalKey shellNavigatorKey = + GlobalKey(debugLabel: 'shell'); + final RouteConfiguration config = RouteConfiguration( + navigatorKey: rootNavigatorKey, + routes: [ + ShellRoute( + builder: (BuildContext context, GoRouterState state, Widget child) { + return _HomeScreen(child: child); + }, + navigatorKey: shellNavigatorKey, + restorationScopeId: 'scope1', + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, + ), + ], + ), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + + final RouteMatchList matches = RouteMatchList( + matches: [ + createRouteMatch(config.routes.first, ''), + createRouteMatch(config.routes.first.routes.first, '/a'), + ], + uri: Uri.parse('/b'), + pathParameters: const {}, + ); + + await tester.pumpWidget( + _BuilderTestWidget( + routeConfiguration: config, + matches: matches, + ), + ); + + expect(find.byKey(rootNavigatorKey), findsOneWidget); + expect(find.byKey(shellNavigatorKey), findsOneWidget); + expect( + (shellNavigatorKey.currentWidget as Navigator?)?.restorationScopeId, + 'scope1'); + }); + + testWidgets('Uses the correct restorationScopeId for StatefulShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(debugLabel: 'root'); + final GlobalKey shellNavigatorKey = + GlobalKey(debugLabel: 'shell'); + final GoRouter goRouter = GoRouter( + initialLocation: '/a', + navigatorKey: rootNavigatorKey, + routes: [ + StatefulShellRoute.indexedStack( + restorationScopeId: 'shell', + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) => + _HomeScreen(child: navigationShell), + branches: [ + StatefulShellBranch( + navigatorKey: shellNavigatorKey, + restorationScopeId: 'scope1', + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) { + return _DetailsScreen(); + }, + ), + ], + ), + ], + ), + ], + ); + + await tester.pumpWidget(MaterialApp.router( + routerConfig: goRouter, + )); + + expect(find.byKey(rootNavigatorKey), findsOneWidget); + expect(find.byKey(shellNavigatorKey), findsOneWidget); + expect( + (shellNavigatorKey.currentWidget as Navigator?)?.restorationScopeId, + 'scope1'); + }); }); } @@ -340,15 +437,15 @@ class _BuilderTestWidget extends StatelessWidget { }, restorationScopeId: null, observers: [], + onPopPageWithRouteMatch: (_, __, ___) => false, ); } @override Widget build(BuildContext context) { return MaterialApp( - home: builder.tryBuild(context, matches, (_, __) => false, false, + home: builder.tryBuild(context, matches, false, routeConfiguration.navigatorKey, , GoRouterState>{}), - // builder: (context, child) => , ); } } diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 08cf53c776da..312470221842 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -6,6 +6,8 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/src/configuration.dart'; +import 'test_helpers.dart'; + void main() { group('RouteConfiguration', () { test('throws when parentNavigatorKey is not an ancestor', () { @@ -80,6 +82,483 @@ void main() { ); }); + test( + 'throws when StatefulShellRoute sub-route uses incorrect parentNavigatorKey', + () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey keyA = + GlobalKey(debugLabel: 'A'); + final GlobalKey keyB = + GlobalKey(debugLabel: 'B'); + + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute.indexedStack(branches: [ + StatefulShellBranch( + navigatorKey: keyA, + routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'details', + builder: _mockScreenBuilder, + parentNavigatorKey: keyB), + ]), + ], + ), + ], builder: mockStackedShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + + test( + 'does not throw when StatefulShellRoute sub-route uses correct parentNavigatorKeys', + () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey keyA = + GlobalKey(debugLabel: 'A'); + + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute.indexedStack(branches: [ + StatefulShellBranch( + navigatorKey: keyA, + routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'details', + builder: _mockScreenBuilder, + parentNavigatorKey: keyA), + ]), + ], + ), + ], builder: mockStackedShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }); + + test( + 'throws when a sub-route of StatefulShellRoute has a parentNavigatorKey', + () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey someNavigatorKey = + GlobalKey(); + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute.indexedStack(branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'details', + builder: _mockScreenBuilder, + parentNavigatorKey: someNavigatorKey), + ]), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/b', + builder: _mockScreenBuilder, + parentNavigatorKey: someNavigatorKey), + ], + ), + ], builder: mockStackedShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + + test('throws when StatefulShellRoute has duplicate navigator keys', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey keyA = + GlobalKey(debugLabel: 'A'); + final List shellRouteChildren = [ + GoRoute( + path: '/a', builder: _mockScreenBuilder, parentNavigatorKey: keyA), + GoRoute( + path: '/b', builder: _mockScreenBuilder, parentNavigatorKey: keyA), + ]; + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute.indexedStack(branches: [ + StatefulShellBranch(routes: shellRouteChildren) + ], builder: mockStackedShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + + test( + 'throws when a child of StatefulShellRoute has an incorrect ' + 'parentNavigatorKey', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + final GoRoute routeA = GoRoute( + path: '/a', + builder: _mockScreenBuilder, + parentNavigatorKey: sectionBNavigatorKey); + final GoRoute routeB = GoRoute( + path: '/b', + builder: _mockScreenBuilder, + parentNavigatorKey: sectionANavigatorKey); + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute.indexedStack(branches: [ + StatefulShellBranch( + routes: [routeA], + navigatorKey: sectionANavigatorKey), + StatefulShellBranch( + routes: [routeB], + navigatorKey: sectionBNavigatorKey), + ], builder: mockStackedShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + + test( + 'throws when a branch of a StatefulShellRoute has an incorrect ' + 'initialLocation', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute.indexedStack(branches: [ + StatefulShellBranch( + initialLocation: '/x', + navigatorKey: sectionANavigatorKey, + routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + ), + ], + ), + StatefulShellBranch( + navigatorKey: sectionBNavigatorKey, + routes: [ + GoRoute( + path: '/b', + builder: _mockScreenBuilder, + ), + ], + ), + ], builder: mockStackedShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + + test( + 'throws when a branch of a StatefulShellRoute has a initialLocation ' + 'that is not a descendant of the same branch', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + expect( + () { + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute.indexedStack(branches: [ + StatefulShellBranch( + initialLocation: '/b', + navigatorKey: sectionANavigatorKey, + routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + ), + ], + ), + StatefulShellBranch( + initialLocation: '/b', + navigatorKey: sectionBNavigatorKey, + routes: [ + StatefulShellRoute.indexedStack( + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/b', + builder: _mockScreenBuilder, + ), + ], + ), + ], + builder: mockStackedShellBuilder), + ], + ), + ], builder: mockStackedShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }, + throwsAssertionError, + ); + }); + + test( + 'does not throw when a branch of a StatefulShellRoute has correctly ' + 'configured initialLocations', () { + final GlobalKey root = + GlobalKey(debugLabel: 'root'); + + RouteConfiguration( + navigatorKey: root, + routes: [ + StatefulShellRoute.indexedStack(branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ], + ), + StatefulShellBranch( + initialLocation: '/b/detail', + routes: [ + GoRoute( + path: '/b', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ], + ), + StatefulShellBranch( + initialLocation: '/c/detail', + routes: [ + StatefulShellRoute.indexedStack(branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/c', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ], + ), + StatefulShellBranch( + initialLocation: '/d/detail', + routes: [ + GoRoute( + path: '/d', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'detail', + builder: _mockScreenBuilder, + ), + ]), + ], + ), + ], builder: mockStackedShellBuilder), + ], + ), + StatefulShellBranch(routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + GoRoute( + path: '/e', + builder: _mockScreenBuilder, + ), + ], + ) + ], + ), + ]), + ], builder: mockStackedShellBuilder), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + }); + + test( + 'derives the correct initialLocation for a StatefulShellBranch', + () { + final StatefulShellBranch branchA; + final StatefulShellBranch branchY; + final StatefulShellBranch branchB; + + final RouteConfiguration config = RouteConfiguration( + navigatorKey: GlobalKey(debugLabel: 'root'), + routes: [ + StatefulShellRoute.indexedStack( + builder: mockStackedShellBuilder, + branches: [ + branchA = StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: _mockScreenBuilder, + routes: [ + GoRoute( + path: 'x', + builder: _mockScreenBuilder, + routes: [ + StatefulShellRoute.indexedStack( + builder: mockStackedShellBuilder, + branches: [ + branchY = + StatefulShellBranch(routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + GoRoute( + path: 'y1', + builder: _mockScreenBuilder, + ), + GoRoute( + path: 'y2', + builder: _mockScreenBuilder, + ), + ]) + ]) + ]), + ], + ), + ], + ), + ]), + branchB = StatefulShellBranch(routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + ShellRoute( + builder: _mockShellBuilder, + routes: [ + GoRoute( + path: '/b1', + builder: _mockScreenBuilder, + ), + GoRoute( + path: '/b2', + builder: _mockScreenBuilder, + ), + ], + ) + ], + ), + ]), + ], + ), + ], + redirectLimit: 10, + topRedirect: (BuildContext context, GoRouterState state) { + return null; + }, + ); + + String? initialLocation(StatefulShellBranch branch) { + final GoRoute? route = branch.defaultRoute; + return route != null ? config.locationForRoute(route) : null; + } + + expect('/a', initialLocation(branchA)); + expect('/a/x/y1', initialLocation(branchY)); + expect('/b1', initialLocation(branchB)); + }, + ); + test( 'throws when there is a GoRoute ancestor with a different parentNavigatorKey', () { diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index a0533a1a539e..c25877bf1453 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -8,6 +8,8 @@ import 'package:go_router/go_router.dart'; import 'package:go_router/src/match.dart'; import 'package:go_router/src/misc/error_screen.dart'; +import 'test_helpers.dart'; + Future createGoRouter( WidgetTester tester, { Listenable? refreshListenable, @@ -30,6 +32,46 @@ Future createGoRouter( return router; } +Future createGoRouterWithStatefulShellRoute( + WidgetTester tester) async { + final GoRouter router = GoRouter( + initialLocation: '/', + routes: [ + GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), + GoRoute(path: '/a', builder: (_, __) => const DummyStatefulWidget()), + StatefulShellRoute.indexedStack(branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/c', + builder: (_, __) => const DummyStatefulWidget(), + routes: [ + GoRoute( + path: 'c1', + builder: (_, __) => const DummyStatefulWidget()), + GoRoute( + path: 'c2', + builder: (_, __) => const DummyStatefulWidget()), + ]), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/d', + builder: (_, __) => const DummyStatefulWidget(), + routes: [ + GoRoute( + path: 'd1', + builder: (_, __) => const DummyStatefulWidget()), + ]), + ]), + ], builder: mockStackedShellBuilder), + ], + ); + await tester.pumpWidget(MaterialApp.router( + routerConfig: router, + )); + return router; +} + void main() { group('pop', () { testWidgets('removes the last element', (WidgetTester tester) async { @@ -79,6 +121,66 @@ void main() { ); }, ); + + testWidgets( + 'It should successfully push a route from outside the the current ' + 'StatefulShellRoute', + (WidgetTester tester) async { + final GoRouter goRouter = + await createGoRouterWithStatefulShellRoute(tester); + goRouter.push('/c/c1'); + await tester.pumpAndSettle(); + + goRouter.push('/a'); + await tester.pumpAndSettle(); + + expect(goRouter.routerDelegate.matches.matches.length, 3); + expect( + goRouter.routerDelegate.matches.matches[2].pageKey, + const Key('/a-p0'), + ); + }, + ); + + testWidgets( + 'It should successfully push a route that is a descendant of the current ' + 'StatefulShellRoute branch', + (WidgetTester tester) async { + final GoRouter goRouter = + await createGoRouterWithStatefulShellRoute(tester); + goRouter.push('/c/c1'); + await tester.pumpAndSettle(); + + goRouter.push('/c/c2'); + await tester.pumpAndSettle(); + + expect(goRouter.routerDelegate.matches.matches.length, 3); + expect( + goRouter.routerDelegate.matches.matches[2].pageKey, + const Key('/c/c2-p0'), + ); + }, + ); + + testWidgets( + 'It should successfully push the root of the current StatefulShellRoute ' + 'branch upon itself', + (WidgetTester tester) async { + final GoRouter goRouter = + await createGoRouterWithStatefulShellRoute(tester); + goRouter.push('/c'); + await tester.pumpAndSettle(); + + goRouter.push('/c'); + await tester.pumpAndSettle(); + + expect(goRouter.routerDelegate.matches.matches.length, 3); + expect( + goRouter.routerDelegate.matches.matches[2].pageKey, + const Key('/c-p1'), + ); + }, + ); }); group('canPop', () { diff --git a/packages/go_router/test/extension_test.dart b/packages/go_router/test/extension_test.dart new file mode 100644 index 000000000000..df12172074ba --- /dev/null +++ b/packages/go_router/test/extension_test.dart @@ -0,0 +1,56 @@ +// 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:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; + +void main() { + group('replaceNamed', () { + Future createGoRouter( + WidgetTester tester, { + Listenable? refreshListenable, + }) async { + final GoRouter router = GoRouter( + initialLocation: '/', + routes: [ + GoRoute( + path: '/', + name: 'home', + builder: (_, __) => const _MyWidget(), + ), + GoRoute( + path: '/page-0/:tab', + name: 'page-0', + builder: (_, __) => const SizedBox()) + ], + ); + await tester.pumpWidget(MaterialApp.router( + routerConfig: router, + )); + return router; + } + + testWidgets('Passes GoRouter parameters through context call.', + (WidgetTester tester) async { + final GoRouter router = await createGoRouter(tester); + await tester.tap(find.text('Settings')); + await tester.pumpAndSettle(); + expect(router.location, '/page-0/settings?search=notification'); + }); + }); +} + +class _MyWidget extends StatelessWidget { + const _MyWidget(); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: () => context.replaceNamed('page-0', + pathParameters: {'tab': 'settings'}, + queryParameters: {'search': 'notification'}), + child: const Text('Settings')); + } +} diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index a93b49213a6c..532a4f64f015 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2504,6 +2504,87 @@ void main() { expect(imperativeRouteMatch.matches.pathParameters['pid'], pid); }); + testWidgets('StatefulShellRoute supports nested routes with params', + (WidgetTester tester) async { + StatefulNavigationShell? routeState; + final List routes = [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; + }, + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/family', + builder: (BuildContext context, GoRouterState state) => + const Text('Families'), + routes: [ + GoRoute( + path: ':fid', + builder: (BuildContext context, GoRouterState state) => + FamilyScreen(state.pathParameters['fid']!), + routes: [ + GoRoute( + path: 'person/:pid', + builder: + (BuildContext context, GoRouterState state) { + final String fid = state.pathParameters['fid']!; + final String pid = state.pathParameters['pid']!; + + return PersonScreen(fid, pid); + }, + ), + ], + ) + ]), + ], + ), + ], + ), + ]; + + final GoRouter router = + await createRouter(routes, tester, initialLocation: '/a'); + const String fid = 'f1'; + const String pid = 'p2'; + const String loc = '/family/$fid/person/$pid'; + + router.go(loc); + await tester.pumpAndSettle(); + RouteMatchList matches = router.routerDelegate.matches; + + expect(router.location, loc); + expect(matches.matches, hasLength(4)); + expect(find.byType(PersonScreen), findsOneWidget); + expect(matches.pathParameters['fid'], fid); + expect(matches.pathParameters['pid'], pid); + + routeState?.goBranch(0); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsOneWidget); + expect(find.byType(PersonScreen), findsNothing); + + routeState?.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.byType(PersonScreen), findsOneWidget); + matches = router.routerDelegate.matches; + expect(matches.pathParameters['fid'], fid); + expect(matches.pathParameters['pid'], pid); + }); + testWidgets('goNames should allow dynamics values for queryParams', (WidgetTester tester) async { const Map queryParametersAll = >{ @@ -2993,187 +3074,953 @@ void main() { expect(find.text('Screen B'), findsOneWidget); expect(find.text('Screen C'), findsNothing); }); - }); - group('Imperative navigation', () { - group('canPop', () { - testWidgets( - 'It should return false if Navigator.canPop() returns false.', - (WidgetTester tester) async { - final GlobalKey navigatorKey = - GlobalKey(); - final GoRouter router = GoRouter( - initialLocation: '/', - navigatorKey: navigatorKey, - routes: [ + testWidgets('Builds StatefulShellRoute', (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + + final List routes = [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) => + navigationShell, + branches: [ + StatefulShellBranch(routes: [ GoRoute( - path: '/', - builder: (BuildContext context, _) { - return Scaffold( - body: TextButton( - onPressed: () async { - navigatorKey.currentState!.push( - MaterialPageRoute( - builder: (BuildContext context) { - return const Scaffold( - body: Text('pageless route'), - ); - }, - ), - ); - }, - child: const Text('Push'), - ), - ); - }, + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), ), - GoRoute(path: '/a', builder: (_, __) => const DummyScreen()), - ], - ); - - await tester.pumpWidget( - MaterialApp.router( - routeInformationProvider: router.routeInformationProvider, - routeInformationParser: router.routeInformationParser, - routerDelegate: router.routerDelegate), - ); - - expect(router.canPop(), false); + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ]), + ], + ), + ]; - await tester.tap(find.text('Push')); - await tester.pumpAndSettle(); + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a', navigatorKey: rootNavigatorKey); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen B'), findsNothing); - expect( - find.text('pageless route', skipOffstage: false), findsOneWidget); - expect(router.canPop(), true); - }, - ); + router.go('/b'); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B'), findsOneWidget); + }); - testWidgets( - 'It checks if ShellRoute navigators can pop', + testWidgets('Builds StatefulShellRoute as a sub-route', (WidgetTester tester) async { - final GlobalKey shellNavigatorKey = - GlobalKey(); - final GoRouter router = GoRouter( - initialLocation: '/a', - routes: [ - ShellRoute( - navigatorKey: shellNavigatorKey, - builder: - (BuildContext context, GoRouterState state, Widget child) { - return Scaffold( - appBar: AppBar(title: const Text('Shell')), - body: child, - ); - }, - routes: [ + final GlobalKey rootNavigatorKey = + GlobalKey(); + + final List routes = [ + GoRoute( + path: '/root', + builder: (BuildContext context, GoRouterState state) => + const Text('Root'), + routes: [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) => + navigationShell, + branches: [ + StatefulShellBranch(routes: [ GoRoute( - path: '/a', - builder: (BuildContext context, _) { - return Scaffold( - body: TextButton( - onPressed: () async { - shellNavigatorKey.currentState!.push( - MaterialPageRoute( - builder: (BuildContext context) { - return const Scaffold( - body: Text('pageless route'), - ); - }, - ), - ); - }, - child: const Text('Push'), - ), - ); - }, + path: 'a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), ), - ], - ), - ], - ); - - await tester.pumpWidget( - MaterialApp.router( - routeInformationProvider: router.routeInformationProvider, - routeInformationParser: router.routeInformationParser, - routerDelegate: router.routerDelegate), - ); - - expect(router.canPop(), false); - expect(find.text('Push'), findsOneWidget); - - await tester.tap(find.text('Push')); - await tester.pumpAndSettle(); - - expect( - find.text('pageless route', skipOffstage: false), findsOneWidget); - expect(router.canPop(), true); - }, - ); - - testWidgets( - 'It checks if ShellRoute navigators can pop', - (WidgetTester tester) async { - final GlobalKey shellNavigatorKey = - GlobalKey(); - final GoRouter router = GoRouter( - initialLocation: '/a', - routes: [ - ShellRoute( - navigatorKey: shellNavigatorKey, - builder: - (BuildContext context, GoRouterState state, Widget child) { - return Scaffold( - appBar: AppBar(title: const Text('Shell')), - body: child, - ); - }, - routes: [ + ]), + StatefulShellBranch(routes: [ GoRoute( - path: '/a', - builder: (BuildContext context, _) { - return Scaffold( - body: TextButton( - onPressed: () async { - shellNavigatorKey.currentState!.push( - MaterialPageRoute( - builder: (BuildContext context) { - return const Scaffold( - body: Text('pageless route'), - ); - }, - ), - ); - }, - child: const Text('Push'), - ), - ); - }, + path: 'b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), ), - ], - ), - ], - ); + ]), + ], + ), + ], + ), + ]; - await tester.pumpWidget( - MaterialApp.router( - routeInformationProvider: router.routeInformationProvider, - routeInformationParser: router.routeInformationParser, - routerDelegate: router.routerDelegate), - ); + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/root/a', navigatorKey: rootNavigatorKey); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen B'), findsNothing); - expect(router.canPop(), false); - expect(find.text('Push'), findsOneWidget); + router.go('/root/b'); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B'), findsOneWidget); + }); - await tester.tap(find.text('Push')); - await tester.pumpAndSettle(); + testWidgets( + 'Navigation with goBranch is correctly handled in StatefulShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey statefulWidgetKey = + GlobalKey(); + StatefulNavigationShell? routeState; - expect( + final List routes = [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; + }, + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/c', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen C'), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: '/d', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen D'), + ), + ], + ), + ], + ), + ]; + + await createRouter(routes, tester, + initialLocation: '/a', navigatorKey: rootNavigatorKey); + statefulWidgetKey.currentState?.increment(); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen C'), findsNothing); + expect(find.text('Screen D'), findsNothing); + + routeState!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B'), findsOneWidget); + expect(find.text('Screen C'), findsNothing); + expect(find.text('Screen D'), findsNothing); + + routeState!.goBranch(2); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen C'), findsOneWidget); + expect(find.text('Screen D'), findsNothing); + + routeState!.goBranch(3); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen C'), findsNothing); + expect(find.text('Screen D'), findsOneWidget); + + expect(() { + // Verify that navigation to unknown index fails + routeState!.goBranch(4); + }, throwsA(isA())); + }); + + testWidgets( + 'Navigates to correct nested navigation tree in StatefulShellRoute ' + 'and maintains state', (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey statefulWidgetKey = + GlobalKey(); + StatefulNavigationShell? routeState; + + final List routes = [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; + }, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detailA', + builder: (BuildContext context, GoRouterState state) => + Column(children: [ + const Text('Screen A Detail'), + DummyStatefulWidget(key: statefulWidgetKey), + ]), + ), + ], + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ]), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a/detailA', navigatorKey: rootNavigatorKey); + statefulWidgetKey.currentState?.increment(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(find.text('Screen B'), findsNothing); + + routeState!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsNothing); + expect(find.text('Screen B'), findsOneWidget); + + routeState!.goBranch(0); + await tester.pumpAndSettle(); + expect(statefulWidgetKey.currentState?.counter, equals(1)); + + router.go('/a'); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen A Detail'), findsNothing); + router.go('/a/detailA'); + await tester.pumpAndSettle(); + expect(statefulWidgetKey.currentState?.counter, equals(0)); + }); + + testWidgets('Maintains state for nested StatefulShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey statefulWidgetKey = + GlobalKey(); + StatefulNavigationShell? routeState1; + StatefulNavigationShell? routeState2; + + final List routes = [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + routeState1 = navigationShell; + return navigationShell; + }, + branches: [ + StatefulShellBranch(routes: [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + routeState2 = navigationShell; + return navigationShell; + }, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detailA', + builder: + (BuildContext context, GoRouterState state) => + Column(children: [ + const Text('Screen A Detail'), + DummyStatefulWidget(key: statefulWidgetKey), + ]), + ), + ], + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/c', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen C'), + ), + ]), + ]), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/d', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen D'), + ), + ]), + ], + ), + ]; + + await createRouter(routes, tester, + initialLocation: '/a/detailA', navigatorKey: rootNavigatorKey); + statefulWidgetKey.currentState?.increment(); + expect(find.text('Screen A Detail'), findsOneWidget); + routeState2!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen B'), findsOneWidget); + + routeState1!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen D'), findsOneWidget); + + routeState1!.goBranch(0); + await tester.pumpAndSettle(); + expect(find.text('Screen B'), findsOneWidget); + + routeState2!.goBranch(2); + await tester.pumpAndSettle(); + expect(find.text('Screen C'), findsOneWidget); + + routeState2!.goBranch(0); + await tester.pumpAndSettle(); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(statefulWidgetKey.currentState?.counter, equals(1)); + }); + + testWidgets( + 'Pops from the correct Navigator in a StatefulShellRoute when the ' + 'Android back button is pressed', (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey sectionANavigatorKey = + GlobalKey(); + final GlobalKey sectionBNavigatorKey = + GlobalKey(); + StatefulNavigationShell? routeState; + + final List routes = [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; + }, + branches: [ + StatefulShellBranch( + navigatorKey: sectionANavigatorKey, + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + routes: [ + GoRoute( + path: 'detailA', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A Detail'), + ), + ], + ), + ]), + StatefulShellBranch( + navigatorKey: sectionBNavigatorKey, + routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + routes: [ + GoRoute( + path: 'detailB', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail'), + ), + ], + ), + ]), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a/detailA', navigatorKey: rootNavigatorKey); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen B Detail'), findsNothing); + + router.go('/b/detailB'); + await tester.pumpAndSettle(); + + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsNothing); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen B Detail'), findsOneWidget); + + await simulateAndroidBackButton(tester); + await tester.pumpAndSettle(); + + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsNothing); + expect(find.text('Screen B'), findsOneWidget); + expect(find.text('Screen B Detail'), findsNothing); + + routeState!.goBranch(0); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen A Detail'), findsOneWidget); + + await simulateAndroidBackButton(tester); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen A Detail'), findsNothing); + }); + + testWidgets( + 'Maintains extra navigation information when navigating ' + 'between branches in StatefulShellRoute', (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + StatefulNavigationShell? routeState; + + final List routes = [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; + }, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + Text('Screen B - ${state.extra}'), + ), + ]), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a', navigatorKey: rootNavigatorKey); + expect(find.text('Screen A'), findsOneWidget); + + router.go('/b', extra: 'X'); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B - X'), findsOneWidget); + + routeState!.goBranch(0); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen B - X'), findsNothing); + + routeState!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B - X'), findsOneWidget); + }); + + testWidgets( + 'Pushed non-descendant routes are correctly restored when ' + 'navigating between branches in StatefulShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + StatefulNavigationShell? routeState; + + final List routes = [ + GoRoute( + path: '/common', + builder: (BuildContext context, GoRouterState state) => + Text('Common - ${state.extra}'), + ), + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; + }, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + ), + ]), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a', navigatorKey: rootNavigatorKey); + expect(find.text('Screen A'), findsOneWidget); + + router.go('/b'); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B'), findsOneWidget); + + router.push('/common', extra: 'X'); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Common - X'), findsOneWidget); + + routeState!.goBranch(0); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsOneWidget); + + routeState!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Common - X'), findsOneWidget); + }); + + testWidgets( + 'Redirects are correctly handled when switching branch in a ' + 'StatefulShellRoute', (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + StatefulNavigationShell? routeState; + + final List routes = [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; + }, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B'), + routes: [ + GoRoute( + path: 'details1', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail1'), + ), + GoRoute( + path: 'details2', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B Detail2'), + ), + ], + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/c', + redirect: (_, __) => '/c/main2', + ), + GoRoute( + path: '/c/main1', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen C1'), + ), + GoRoute( + path: '/c/main2', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen C2'), + ), + ]), + ], + ), + ]; + + String redirectDestinationBranchB = '/b/details1'; + await createRouter( + routes, + tester, + initialLocation: '/a', + navigatorKey: rootNavigatorKey, + redirect: (_, GoRouterState state) { + if (state.location.startsWith('/b')) { + return redirectDestinationBranchB; + } + return null; + }, + ); + expect(find.text('Screen A'), findsOneWidget); + expect(find.text('Screen B Detail'), findsNothing); + + routeState!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B Detail1'), findsOneWidget); + + routeState!.goBranch(2); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B Detail1'), findsNothing); + expect(find.text('Screen C2'), findsOneWidget); + + redirectDestinationBranchB = '/b/details2'; + routeState!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B Detail2'), findsOneWidget); + expect(find.text('Screen C2'), findsNothing); + }); + + testWidgets( + 'Pushed top-level route is correctly handled by StatefulShellRoute', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey nestedNavigatorKey = + GlobalKey(); + StatefulNavigationShell? routeState; + + final List routes = [ + // First level shell + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + routeState = navigationShell; + return navigationShell; + }, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen A'), + ), + ]), + StatefulShellBranch(routes: [ + // Second level / nested shell + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) => + navigationShell, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/b1', + builder: (BuildContext context, GoRouterState state) => + const Text('Screen B1'), + ), + ]), + StatefulShellBranch( + navigatorKey: nestedNavigatorKey, + routes: [ + GoRoute( + path: '/b2', + builder: + (BuildContext context, GoRouterState state) => + const Text('Screen B2'), + ), + GoRoute( + path: '/b2-modal', + // We pass an explicit parentNavigatorKey here, to + // properly test the logic in RouteBuilder, i.e. + // routes with parentNavigatorKeys under the shell + // should not be stripped. + parentNavigatorKey: nestedNavigatorKey, + builder: + (BuildContext context, GoRouterState state) => + const Text('Nested Modal'), + ), + ]), + ], + ), + ]), + ], + ), + GoRoute( + path: '/top-modal', + parentNavigatorKey: rootNavigatorKey, + builder: (BuildContext context, GoRouterState state) => + const Text('Top Modal'), + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a', navigatorKey: rootNavigatorKey); + expect(find.text('Screen A'), findsOneWidget); + + routeState!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen B1'), findsOneWidget); + + // Navigate nested (second level) shell to second branch + router.go('/b2'); + await tester.pumpAndSettle(); + expect(find.text('Screen B2'), findsOneWidget); + + // Push route over second branch of nested (second level) shell + router.push('/b2-modal'); + await tester.pumpAndSettle(); + expect(find.text('Nested Modal'), findsOneWidget); + + // Push top-level route while on second branch + router.push('/top-modal'); + await tester.pumpAndSettle(); + expect(find.text('Top Modal'), findsOneWidget); + + // Return to shell and first branch + router.go('/a'); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsOneWidget); + + // Switch to second branch, which should only contain 'Nested Modal' + // (in the nested shell) + routeState!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen A'), findsNothing); + expect(find.text('Screen B1'), findsNothing); + expect(find.text('Screen B2'), findsNothing); + expect(find.text('Top Modal'), findsNothing); + expect(find.text('Nested Modal'), findsOneWidget); + }); + }); + + group('Imperative navigation', () { + group('canPop', () { + testWidgets( + 'It should return false if Navigator.canPop() returns false.', + (WidgetTester tester) async { + final GlobalKey navigatorKey = + GlobalKey(); + final GoRouter router = GoRouter( + initialLocation: '/', + navigatorKey: navigatorKey, + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, _) { + return Scaffold( + body: TextButton( + onPressed: () async { + navigatorKey.currentState!.push( + MaterialPageRoute( + builder: (BuildContext context) { + return const Scaffold( + body: Text('pageless route'), + ); + }, + ), + ); + }, + child: const Text('Push'), + ), + ); + }, + ), + GoRoute(path: '/a', builder: (_, __) => const DummyScreen()), + ], + ); + + await tester.pumpWidget( + MaterialApp.router( + routeInformationProvider: router.routeInformationProvider, + routeInformationParser: router.routeInformationParser, + routerDelegate: router.routerDelegate), + ); + + expect(router.canPop(), false); + + await tester.tap(find.text('Push')); + await tester.pumpAndSettle(); + + expect( + find.text('pageless route', skipOffstage: false), findsOneWidget); + expect(router.canPop(), true); + }, + ); + + testWidgets( + 'It checks if ShellRoute navigators can pop', + (WidgetTester tester) async { + final GlobalKey shellNavigatorKey = + GlobalKey(); + final GoRouter router = GoRouter( + initialLocation: '/a', + routes: [ + ShellRoute( + navigatorKey: shellNavigatorKey, + builder: + (BuildContext context, GoRouterState state, Widget child) { + return Scaffold( + appBar: AppBar(title: const Text('Shell')), + body: child, + ); + }, + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, _) { + return Scaffold( + body: TextButton( + onPressed: () async { + shellNavigatorKey.currentState!.push( + MaterialPageRoute( + builder: (BuildContext context) { + return const Scaffold( + body: Text('pageless route'), + ); + }, + ), + ); + }, + child: const Text('Push'), + ), + ); + }, + ), + ], + ), + ], + ); + + await tester.pumpWidget( + MaterialApp.router( + routeInformationProvider: router.routeInformationProvider, + routeInformationParser: router.routeInformationParser, + routerDelegate: router.routerDelegate), + ); + + expect(router.canPop(), false); + expect(find.text('Push'), findsOneWidget); + + await tester.tap(find.text('Push')); + await tester.pumpAndSettle(); + + expect( find.text('pageless route', skipOffstage: false), findsOneWidget); expect(router.canPop(), true); }, ); + + testWidgets( + 'It checks if StatefulShellRoute navigators can pop', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GoRouter router = GoRouter( + navigatorKey: rootNavigatorKey, + initialLocation: '/a', + routes: [ + StatefulShellRoute.indexedStack( + builder: mockStackedShellBuilder, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, _) { + return const Scaffold( + body: Text('Screen A'), + ); + }, + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b', + builder: (BuildContext context, _) { + return const Scaffold( + body: Text('Screen B'), + ); + }, + routes: [ + GoRoute( + path: 'detail', + builder: (BuildContext context, _) { + return const Scaffold( + body: Text('Screen B detail'), + ); + }, + ), + ], + ), + ]), + ], + ), + ], + ); + + await tester.pumpWidget( + MaterialApp.router( + routeInformationProvider: router.routeInformationProvider, + routeInformationParser: router.routeInformationParser, + routerDelegate: router.routerDelegate), + ); + + expect(router.canPop(), false); + + router.go('/b/detail'); + await tester.pumpAndSettle(); + + expect(find.text('Screen B detail', skipOffstage: false), + findsOneWidget); + expect(router.canPop(), true); + // Verify that it is actually the StatefulShellRoute that reports + // canPop = true + expect(rootNavigatorKey.currentState?.canPop(), false); + }, + ); + testWidgets('Pageless route should include in can pop', (WidgetTester tester) async { final GlobalKey root = @@ -3483,6 +4330,321 @@ void main() { }, ); }); + + group('state restoration', () { + testWidgets('Restores state correctly', (WidgetTester tester) async { + final GlobalKey statefulWidgetKeyA = + GlobalKey(); + + final List routes = [ + GoRoute( + path: '/a', + pageBuilder: createPageBuilder( + restorationId: 'screenA', child: const Text('Screen A')), + routes: [ + GoRoute( + path: 'detail', + pageBuilder: createPageBuilder( + restorationId: 'screenADetail', + child: Column(children: [ + const Text('Screen A Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyA, restorationId: 'counterA'), + ])), + ), + ], + ), + ]; + + await createRouter(routes, tester, + initialLocation: '/a/detail', restorationScopeId: 'test'); + await tester.pumpAndSettle(); + statefulWidgetKeyA.currentState?.increment(); + expect(statefulWidgetKeyA.currentState?.counter, equals(1)); + await tester.pumpAndSettle(); // Give state change time to persist + + await tester.restartAndRestore(); + + await tester.pumpAndSettle(); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(statefulWidgetKeyA.currentState?.counter, equals(1)); + }); + + testWidgets('Restores state of branches in StatefulShellRoute correctly', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey statefulWidgetKeyA = + GlobalKey(); + final GlobalKey statefulWidgetKeyB = + GlobalKey(); + final GlobalKey statefulWidgetKeyC = + GlobalKey(); + StatefulNavigationShell? routeState; + + final List routes = [ + StatefulShellRoute.indexedStack( + restorationScopeId: 'shell', + pageBuilder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + routeState = navigationShell; + return MaterialPage( + restorationId: 'shellWidget', child: navigationShell); + }, + branches: [ + StatefulShellBranch( + restorationScopeId: 'branchA', + routes: [ + GoRoute( + path: '/a', + pageBuilder: createPageBuilder( + restorationId: 'screenA', + child: const Text('Screen A')), + routes: [ + GoRoute( + path: 'detailA', + pageBuilder: createPageBuilder( + restorationId: 'screenADetail', + child: Column(children: [ + const Text('Screen A Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyA, + restorationId: 'counterA'), + ])), + ), + ], + ), + ]), + StatefulShellBranch( + restorationScopeId: 'branchB', + routes: [ + GoRoute( + path: '/b', + pageBuilder: createPageBuilder( + restorationId: 'screenB', + child: const Text('Screen B')), + routes: [ + GoRoute( + path: 'detailB', + pageBuilder: createPageBuilder( + restorationId: 'screenBDetail', + child: Column(children: [ + const Text('Screen B Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyB, + restorationId: 'counterB'), + ])), + ), + ], + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/c', + pageBuilder: createPageBuilder( + restorationId: 'screenC', child: const Text('Screen C')), + routes: [ + GoRoute( + path: 'detailC', + pageBuilder: createPageBuilder( + restorationId: 'screenCDetail', + child: Column(children: [ + const Text('Screen C Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyC, + restorationId: 'counterC'), + ])), + ), + ], + ), + ]), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a/detailA', + navigatorKey: rootNavigatorKey, + restorationScopeId: 'test'); + await tester.pumpAndSettle(); + statefulWidgetKeyA.currentState?.increment(); + expect(statefulWidgetKeyA.currentState?.counter, equals(1)); + + router.go('/b/detailB'); + await tester.pumpAndSettle(); + statefulWidgetKeyB.currentState?.increment(); + expect(statefulWidgetKeyB.currentState?.counter, equals(1)); + + router.go('/c/detailC'); + await tester.pumpAndSettle(); + statefulWidgetKeyC.currentState?.increment(); + expect(statefulWidgetKeyC.currentState?.counter, equals(1)); + + routeState!.goBranch(0); + await tester.pumpAndSettle(); + expect(find.text('Screen A Detail'), findsOneWidget); + + await tester.restartAndRestore(); + + await tester.pumpAndSettle(); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(statefulWidgetKeyA.currentState?.counter, equals(1)); + + routeState!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen B Detail'), findsOneWidget); + expect(statefulWidgetKeyB.currentState?.counter, equals(1)); + + routeState!.goBranch(2); + await tester.pumpAndSettle(); + expect(find.text('Screen C Detail'), findsOneWidget); + // State of branch C should not have been restored + expect(statefulWidgetKeyC.currentState?.counter, equals(0)); + }); + + testWidgets( + 'Restores state of imperative routes in StatefulShellRoute correctly', + (WidgetTester tester) async { + final GlobalKey rootNavigatorKey = + GlobalKey(); + final GlobalKey statefulWidgetKeyA = + GlobalKey(); + final GlobalKey statefulWidgetKeyB = + GlobalKey(); + StatefulNavigationShell? routeStateRoot; + StatefulNavigationShell? routeStateNested; + + final List routes = [ + StatefulShellRoute.indexedStack( + restorationScopeId: 'shell', + pageBuilder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + routeStateRoot = navigationShell; + return MaterialPage( + restorationId: 'shellWidget', child: navigationShell); + }, + branches: [ + StatefulShellBranch( + restorationScopeId: 'branchA', + routes: [ + GoRoute( + path: '/a', + pageBuilder: createPageBuilder( + restorationId: 'screenA', + child: const Text('Screen A')), + routes: [ + GoRoute( + path: 'detailA', + pageBuilder: createPageBuilder( + restorationId: 'screenADetail', + child: Column(children: [ + const Text('Screen A Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyA, + restorationId: 'counterA'), + ])), + ), + ], + ), + ]), + StatefulShellBranch( + restorationScopeId: 'branchB', + routes: [ + StatefulShellRoute.indexedStack( + restorationScopeId: 'branchB-nested-shell', + pageBuilder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + routeStateNested = navigationShell; + return MaterialPage( + restorationId: 'shellWidget-nested', + child: navigationShell); + }, + branches: [ + StatefulShellBranch( + restorationScopeId: 'branchB-nested', + routes: [ + GoRoute( + path: '/b', + pageBuilder: createPageBuilder( + restorationId: 'screenB', + child: const Text('Screen B')), + routes: [ + GoRoute( + path: 'detailB', + pageBuilder: createPageBuilder( + restorationId: 'screenBDetail', + child: Column(children: [ + const Text('Screen B Detail'), + DummyRestorableStatefulWidget( + key: statefulWidgetKeyB, + restorationId: 'counterB'), + ])), + ), + ], + ), + ]), + StatefulShellBranch( + restorationScopeId: 'branchC-nested', + routes: [ + GoRoute( + path: '/c', + pageBuilder: createPageBuilder( + restorationId: 'screenC', + child: const Text('Screen C')), + ), + ]), + ]) + ]), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester, + initialLocation: '/a/detailA', + navigatorKey: rootNavigatorKey, + restorationScopeId: 'test'); + await tester.pumpAndSettle(); + statefulWidgetKeyA.currentState?.increment(); + expect(statefulWidgetKeyA.currentState?.counter, equals(1)); + + routeStateRoot!.goBranch(1); + await tester.pumpAndSettle(); + + router.go('/b/detailB'); + await tester.pumpAndSettle(); + statefulWidgetKeyB.currentState?.increment(); + expect(statefulWidgetKeyB.currentState?.counter, equals(1)); + + routeStateRoot!.goBranch(0); + await tester.pumpAndSettle(); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen B Pushed Detail'), findsNothing); + + await tester.restartAndRestore(); + + await tester.pumpAndSettle(); + expect(find.text('Screen A Detail'), findsOneWidget); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen B Pushed Detail'), findsNothing); + expect(statefulWidgetKeyA.currentState?.counter, equals(1)); + + routeStateRoot!.goBranch(1); + await tester.pumpAndSettle(); + expect(find.text('Screen A Detail'), findsNothing); + expect(find.text('Screen B'), findsNothing); + expect(find.text('Screen B Detail'), findsOneWidget); + expect(statefulWidgetKeyB.currentState?.counter, equals(1)); + + routeStateNested!.goBranch(1); + await tester.pumpAndSettle(); + routeStateNested!.goBranch(0); + await tester.pumpAndSettle(); + + expect(find.text('Screen B Detail'), findsOneWidget); + expect(statefulWidgetKeyB.currentState?.counter, equals(1)); + }); + }); } class TestInheritedNotifier extends InheritedNotifier> { diff --git a/packages/go_router/test/matching_test.dart b/packages/go_router/test/matching_test.dart index daf8ccc46a16..8a1d12ab5352 100644 --- a/packages/go_router/test/matching_test.dart +++ b/packages/go_router/test/matching_test.dart @@ -74,4 +74,37 @@ void main() { expect(matches1 == matches2, isTrue); expect(matches1 == matches3, isFalse); }); + + test('RouteMatchList is encoded and decoded correctly', () { + final RouteConfiguration configuration = RouteConfiguration( + routes: [ + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const Placeholder(), + ), + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const Placeholder(), + ), + ], + redirectLimit: 0, + navigatorKey: GlobalKey(), + topRedirect: (_, __) => null, + ); + final RouteMatcher matcher = RouteMatcher(configuration); + final RouteMatchListCodec codec = RouteMatchListCodec(matcher); + + final RouteMatchList list1 = matcher.findMatch('/a'); + final RouteMatchList list2 = matcher.findMatch('/b'); + list1.push(ImperativeRouteMatch( + pageKey: const ValueKey('/b-p0'), matches: list2)); + + final Object? encoded = codec.encodeMatchList(list1); + final RouteMatchList? decoded = codec.decodeMatchList(encoded); + + expect(decoded, isNotNull); + expect(decoded, equals(list1)); + }); } diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index d2187d4ce288..19f795ceda77 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -8,6 +8,7 @@ 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/match.dart'; Future createGoRouter(WidgetTester tester) async { final GoRouter goRouter = GoRouter( @@ -147,6 +148,7 @@ Future createRouter( int redirectLimit = 5, GlobalKey? navigatorKey, GoRouterWidgetBuilder? errorBuilder, + String? restorationScopeId, }) async { final GoRouter goRouter = GoRouter( routes: routes, @@ -158,9 +160,12 @@ Future createRouter( (BuildContext context, GoRouterState state) => TestErrorScreen(state.error!), navigatorKey: navigatorKey, + restorationScopeId: restorationScopeId, ); await tester.pumpWidget( MaterialApp.router( + restorationScopeId: + restorationScopeId != null ? '$restorationScopeId-root' : null, routerConfig: goRouter, ), ); @@ -228,10 +233,49 @@ class DummyStatefulWidget extends StatefulWidget { const DummyStatefulWidget({super.key}); @override - State createState() => DummyStatefulWidgetState(); + State createState() => DummyStatefulWidgetState(); } class DummyStatefulWidgetState extends State { + int counter = 0; + + void increment() => setState(() { + counter++; + }); + + @override + Widget build(BuildContext context) => Container(); +} + +class DummyRestorableStatefulWidget extends StatefulWidget { + const DummyRestorableStatefulWidget({super.key, this.restorationId}); + + final String? restorationId; + + @override + State createState() => DummyRestorableStatefulWidgetState(); +} + +class DummyRestorableStatefulWidgetState + extends State with RestorationMixin { + final RestorableInt _counter = RestorableInt(0); + + @override + String? get restorationId => widget.restorationId; + + int get counter => _counter.value; + + void increment([int count = 1]) => setState(() { + _counter.value += count; + }); + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + if (restorationId != null) { + registerForRestoration(_counter, restorationId!); + } + } + @override Widget build(BuildContext context) => Container(); } @@ -242,3 +286,23 @@ Future simulateAndroidBackButton(WidgetTester tester) async { await tester.binding.defaultBinaryMessenger .handlePlatformMessage('flutter/navigation', message, (_) {}); } + +GoRouterPageBuilder createPageBuilder( + {String? restorationId, required Widget child}) => + (BuildContext context, GoRouterState state) => + MaterialPage(restorationId: restorationId, child: child); + +StatefulShellRouteBuilder mockStackedShellBuilder = (BuildContext context, + GoRouterState state, StatefulNavigationShell navigationShell) { + return navigationShell; +}; + +RouteMatch createRouteMatch(RouteBase route, String location) { + return RouteMatch( + route: route, + matchedLocation: location, + extra: null, + error: null, + pageKey: ValueKey(location), + ); +} diff --git a/packages/go_router/tool/run_tests.dart b/packages/go_router/tool/run_tests.dart index 39fe8cc03613..58c689bf9428 100644 --- a/packages/go_router/tool/run_tests.dart +++ b/packages/go_router/tool/run_tests.dart @@ -9,25 +9,92 @@ // ignore_for_file: avoid_print import 'dart:io'; +import 'package:io/io.dart' as io; import 'package:path/path.dart' as p; +// This test runner simulates a consumption of go_router that checks if +// the dart fixes are applied correctly. +// This is done by copying the `test_fixes/` directory to a temp directory +// that references `go_router`, and running `dart fix --compare-to-golden` +// on the temp directory. Future main(List args) async { - if (!Platform.isMacOS) { - print('This test can only be run on macOS.'); - exit(0); + final Directory goRouterPackageRoot = + File.fromUri(Platform.script).parent.parent; + + final Directory testTempDir = await Directory.systemTemp.createTemp(); + + // Cleans up the temp directory and exits with a given statusCode. + Future cleanUpAndExit(int statusCode) async { + await testTempDir.delete(recursive: true); + exit(statusCode); } - final Directory packageRoot = - Directory(p.dirname(Platform.script.path)).parent; - final int status = await _runProcess( + + // Copy the test_fixes folder to the temporary testFixesTargetDir. + // + // This also creates the proper pubspec.yaml in the temp directory. + await _prepareTemplate( + packageRoot: goRouterPackageRoot, + testTempDir: testTempDir, + ); + + // Run dart pub get in the temp directory to set it up. + final int pubGetStatusCode = await _runProcess( + 'dart', + [ + 'pub', + 'get', + ], + workingDirectory: testTempDir.path, + ); + + if (pubGetStatusCode != 0) { + await cleanUpAndExit(pubGetStatusCode); + } + + // Run dart fix --compare-to-golden in the temp directory. + final int dartFixStatusCode = await _runProcess( 'dart', [ 'fix', '--compare-to-golden', ], - workingDirectory: p.join(packageRoot.path, 'test_fixes'), + workingDirectory: testTempDir.path, ); - exit(status); + await cleanUpAndExit(dartFixStatusCode); +} + +Future _prepareTemplate({ + required Directory packageRoot, + required Directory testTempDir, +}) async { + // The src test_fixes directory. + final Directory testFixesSrcDir = + Directory(p.join(packageRoot.path, 'test_fixes')); + + // Copy from src `test_fixes/` to the temp directory. + await io.copyPath(testFixesSrcDir.path, testTempDir.path); + + // The pubspec.yaml file to create. + final File targetPubspecFile = File(p.join(testTempDir.path, 'pubspec.yaml')); + + final String targetYaml = ''' +name: test_fixes +publish_to: "none" +version: 1.0.0 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + go_router: + path: ${packageRoot.path} +'''; + + await targetPubspecFile.writeAsString(targetYaml); } Future _streamOutput(Future processFuture) async { diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index a7414a840e50..bdab45ea9570 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,15 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 2.0.1 + +* Supports name parameter for `TypedGoRoute`. +## 2.0.0 + +* Updates the documentation to go_router v7.0.0. +* Bumps go_router version in example folder to v7.0.0. + ## 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) diff --git a/packages/go_router_builder/README.md b/packages/go_router_builder/README.md index 6632625afc8d..18a4ea0c9419 100644 --- a/packages/go_router_builder/README.md +++ b/packages/go_router_builder/README.md @@ -8,12 +8,12 @@ To use `go_router_builder`, you need to have the following dependencies in ```yaml dependencies: # ...along with your other dependencies - go_router: ^3.1.0 + go_router: ^7.0.0 dev_dependencies: # ...along with your other dev-dependencies build_runner: ^2.0.0 - go_router_builder: ^1.0.0 + go_router_builder: ^2.0.0 ``` ### Source code @@ -46,7 +46,7 @@ Read more about using in a URI format into one or more page builders, each that require zero or more arguments that are passed as path and query parameters as part of the location. `go_router` does a good job of making the path and query parameters available -via the `params` and `queryParams` properties of the `GoRouterState` object, but +via the `pathParameters` and `queryParameters` properties of the `GoRouterState` object, but often the page builder must first parse the parameters into types that aren't `String`s, e.g. @@ -55,7 +55,7 @@ GoRoute( path: ':authorId', builder: (context, state) { // require the authorId to be present and be an integer - final authorId = int.parse(state.params['authorId']!); + final authorId = int.parse(state.pathParameters['authorId']!); return AuthorDetailsScreen(authorId: authorId); }, ), @@ -258,8 +258,8 @@ generator: ```dart redirect: (state) { final loggedIn = loginInfo.loggedIn; - final loggingIn = state.subloc == LoginRoute().location; - if( !loggedIn && !loggingIn ) return LoginRoute(from: state.subloc).location; + final loggingIn = state.matchedLocation == LoginRoute().location; + if( !loggedIn && !loggingIn ) return LoginRoute(from: state.matchedLocation).location; if( loggedIn && loggingIn ) return HomeRoute().location; return null; } @@ -280,7 +280,7 @@ class HomeRoute extends GoRouteData { ## Type conversions The code generator can convert simple types like `int` and `enum` to/from the -`String` type of the underlying params: +`String` type of the underlying pathParameters: ```dart enum BookKind { all, popular, recent } @@ -341,7 +341,7 @@ class FancyRoute extends GoRouteData { ## TypedShellRoute and navigator keys -There may be situations were a child route of a shell needs to be displayed on a +There may be situations where a child route of a shell needs to be displayed on a different navigator. This kind of scenarios can be achieved by declaring a **static** navigator key named: 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 7cf692f6cbe7..8f3add393ebe 100644 --- a/packages/go_router_builder/example/lib/all_types.g.dart +++ b/packages/go_router_builder/example/lib/all_types.g.dart @@ -89,9 +89,10 @@ extension $AllTypesBaseRouteExtension on AllTypesBaseRoute { extension $BigIntRouteExtension on BigIntRoute { static BigIntRoute _fromState(GoRouterState state) => BigIntRoute( - requiredBigIntField: BigInt.parse(state.params['requiredBigIntField']!), - bigIntField: - _$convertMapValue('big-int-field', state.queryParams, BigInt.parse), + requiredBigIntField: + BigInt.parse(state.pathParameters['requiredBigIntField']!), + bigIntField: _$convertMapValue( + 'big-int-field', state.queryParameters, BigInt.parse), ); String get location => GoRouteData.$location( @@ -111,12 +112,13 @@ extension $BigIntRouteExtension on BigIntRoute { extension $BoolRouteExtension on BoolRoute { static BoolRoute _fromState(GoRouterState state) => BoolRoute( - requiredBoolField: _$boolConverter(state.params['requiredBoolField']!), - boolField: - _$convertMapValue('bool-field', state.queryParams, _$boolConverter), + requiredBoolField: + _$boolConverter(state.pathParameters['requiredBoolField']!), + boolField: _$convertMapValue( + 'bool-field', state.queryParameters, _$boolConverter), boolFieldWithDefaultValue: _$convertMapValue( 'bool-field-with-default-value', - state.queryParams, + state.queryParameters, _$boolConverter) ?? true, ); @@ -142,9 +144,9 @@ extension $BoolRouteExtension on BoolRoute { extension $DateTimeRouteExtension on DateTimeRoute { static DateTimeRoute _fromState(GoRouterState state) => DateTimeRoute( requiredDateTimeField: - DateTime.parse(state.params['requiredDateTimeField']!), + DateTime.parse(state.pathParameters['requiredDateTimeField']!), dateTimeField: _$convertMapValue( - 'date-time-field', state.queryParams, DateTime.parse), + 'date-time-field', state.queryParameters, DateTime.parse), ); String get location => GoRouteData.$location( @@ -165,12 +167,13 @@ extension $DateTimeRouteExtension on DateTimeRoute { extension $DoubleRouteExtension on DoubleRoute { static DoubleRoute _fromState(GoRouterState state) => DoubleRoute( - requiredDoubleField: double.parse(state.params['requiredDoubleField']!), - doubleField: - _$convertMapValue('double-field', state.queryParams, double.parse), + requiredDoubleField: + double.parse(state.pathParameters['requiredDoubleField']!), + doubleField: _$convertMapValue( + 'double-field', state.queryParameters, double.parse), doubleFieldWithDefaultValue: _$convertMapValue( 'double-field-with-default-value', - state.queryParams, + state.queryParameters, double.parse) ?? 1.0, ); @@ -195,10 +198,13 @@ extension $DoubleRouteExtension on DoubleRoute { extension $IntRouteExtension on IntRoute { static IntRoute _fromState(GoRouterState state) => IntRoute( - requiredIntField: int.parse(state.params['requiredIntField']!), - intField: _$convertMapValue('int-field', state.queryParams, int.parse), + requiredIntField: int.parse(state.pathParameters['requiredIntField']!), + intField: + _$convertMapValue('int-field', state.queryParameters, int.parse), intFieldWithDefaultValue: _$convertMapValue( - 'int-field-with-default-value', state.queryParams, int.parse) ?? + 'int-field-with-default-value', + state.queryParameters, + int.parse) ?? 1, ); @@ -221,10 +227,13 @@ extension $IntRouteExtension on IntRoute { extension $NumRouteExtension on NumRoute { static NumRoute _fromState(GoRouterState state) => NumRoute( - requiredNumField: num.parse(state.params['requiredNumField']!), - numField: _$convertMapValue('num-field', state.queryParams, num.parse), + requiredNumField: num.parse(state.pathParameters['requiredNumField']!), + numField: + _$convertMapValue('num-field', state.queryParameters, num.parse), numFieldWithDefaultValue: _$convertMapValue( - 'num-field-with-default-value', state.queryParams, num.parse) ?? + 'num-field-with-default-value', + state.queryParameters, + num.parse) ?? 1, ); @@ -248,12 +257,12 @@ extension $NumRouteExtension on NumRoute { extension $EnumRouteExtension on EnumRoute { static EnumRoute _fromState(GoRouterState state) => EnumRoute( requiredEnumField: _$PersonDetailsEnumMap - ._$fromName(state.params['requiredEnumField']!), - enumField: _$convertMapValue( - 'enum-field', state.queryParams, _$PersonDetailsEnumMap._$fromName), + ._$fromName(state.pathParameters['requiredEnumField']!), + enumField: _$convertMapValue('enum-field', state.queryParameters, + _$PersonDetailsEnumMap._$fromName), enumFieldWithDefaultValue: _$convertMapValue( 'enum-field-with-default-value', - state.queryParams, + state.queryParameters, _$PersonDetailsEnumMap._$fromName) ?? PersonDetails.favoriteFood, ); @@ -280,12 +289,12 @@ extension $EnumRouteExtension on EnumRoute { extension $EnhancedEnumRouteExtension on EnhancedEnumRoute { static EnhancedEnumRoute _fromState(GoRouterState state) => EnhancedEnumRoute( requiredEnumField: _$SportDetailsEnumMap - ._$fromName(state.params['requiredEnumField']!), - enumField: _$convertMapValue( - 'enum-field', state.queryParams, _$SportDetailsEnumMap._$fromName), + ._$fromName(state.pathParameters['requiredEnumField']!), + enumField: _$convertMapValue('enum-field', state.queryParameters, + _$SportDetailsEnumMap._$fromName), enumFieldWithDefaultValue: _$convertMapValue( 'enum-field-with-default-value', - state.queryParams, + state.queryParameters, _$SportDetailsEnumMap._$fromName) ?? SportDetails.football, ); @@ -311,10 +320,10 @@ extension $EnhancedEnumRouteExtension on EnhancedEnumRoute { extension $StringRouteExtension on StringRoute { static StringRoute _fromState(GoRouterState state) => StringRoute( - requiredStringField: state.params['requiredStringField']!, - stringField: state.queryParams['string-field'], + requiredStringField: state.pathParameters['requiredStringField']!, + stringField: state.queryParameters['string-field'], stringFieldWithDefaultValue: - state.queryParams['string-field-with-default-value'] ?? + state.queryParameters['string-field-with-default-value'] ?? 'defaultValue', ); @@ -337,8 +346,9 @@ extension $StringRouteExtension on StringRoute { extension $UriRouteExtension on UriRoute { static UriRoute _fromState(GoRouterState state) => UriRoute( - requiredUriField: Uri.parse(state.params['requiredUriField']!), - uriField: _$convertMapValue('uri-field', state.queryParams, Uri.parse), + requiredUriField: Uri.parse(state.pathParameters['requiredUriField']!), + uriField: + _$convertMapValue('uri-field', state.queryParameters, Uri.parse), ); String get location => GoRouteData.$location( diff --git a/packages/go_router_builder/example/lib/main.dart b/packages/go_router_builder/example/lib/main.dart index cf917c59b5ce..a8ff9ad5b6f1 100644 --- a/packages/go_router_builder/example/lib/main.dart +++ b/packages/go_router_builder/example/lib/main.dart @@ -40,13 +40,13 @@ class App extends StatelessWidget { redirect: (BuildContext context, GoRouterState state) { final bool loggedIn = loginInfo.loggedIn; - // check just the subloc in case there are query parameters + // check just the matchedLocation in case there are query parameters final String loginLoc = const LoginRoute().location; - final bool goingToLogin = state.subloc == loginLoc; + final bool goingToLogin = state.matchedLocation == loginLoc; // the user is not logged in and not headed to /login, they need to login if (!loggedIn && !goingToLogin) { - return LoginRoute(fromPage: state.subloc).location; + return LoginRoute(fromPage: state.matchedLocation).location; } // the user is logged in and headed to /login, no need to login again diff --git a/packages/go_router_builder/example/lib/main.g.dart b/packages/go_router_builder/example/lib/main.g.dart index 864e8e121ef8..031877e88906 100644 --- a/packages/go_router_builder/example/lib/main.g.dart +++ b/packages/go_router_builder/example/lib/main.g.dart @@ -57,7 +57,7 @@ extension $HomeRouteExtension on HomeRoute { extension $FamilyRouteExtension on FamilyRoute { static FamilyRoute _fromState(GoRouterState state) => FamilyRoute( - state.params['fid']!, + state.pathParameters['fid']!, ); String get location => GoRouteData.$location( @@ -74,8 +74,8 @@ extension $FamilyRouteExtension on FamilyRoute { extension $PersonRouteExtension on PersonRoute { static PersonRoute _fromState(GoRouterState state) => PersonRoute( - state.params['fid']!, - int.parse(state.params['pid']!), + state.pathParameters['fid']!, + int.parse(state.pathParameters['pid']!), ); String get location => GoRouteData.$location( @@ -93,9 +93,9 @@ extension $PersonRouteExtension on PersonRoute { extension $PersonDetailsRouteExtension on PersonDetailsRoute { static PersonDetailsRoute _fromState(GoRouterState state) => PersonDetailsRoute( - state.params['fid']!, - int.parse(state.params['pid']!), - _$PersonDetailsEnumMap._$fromName(state.params['details']!), + state.pathParameters['fid']!, + int.parse(state.pathParameters['pid']!), + _$PersonDetailsEnumMap._$fromName(state.pathParameters['details']!), $extra: state.extra as int?, ); @@ -114,7 +114,7 @@ extension $PersonDetailsRouteExtension on PersonDetailsRoute { extension $FamilyCountRouteExtension on FamilyCountRoute { static FamilyCountRoute _fromState(GoRouterState state) => FamilyCountRoute( - int.parse(state.params['count']!), + int.parse(state.pathParameters['count']!), ); String get location => GoRouteData.$location( @@ -147,7 +147,7 @@ RouteBase get $loginRoute => GoRouteData.$route( extension $LoginRouteExtension on LoginRoute { static LoginRoute _fromState(GoRouterState state) => LoginRoute( - fromPage: state.queryParams['from-page'], + fromPage: state.queryParameters['from-page'], ); String get location => GoRouteData.$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 a8a51017e33b..b34d274a1aa6 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 @@ -72,7 +72,7 @@ extension $UsersRouteDataExtension on UsersRouteData { extension $UserRouteDataExtension on UserRouteData { static UserRouteData _fromState(GoRouterState state) => UserRouteData( - id: int.parse(state.params['id']!), + id: int.parse(state.pathParameters['id']!), ); String get location => GoRouteData.$location( diff --git a/packages/go_router_builder/example/lib/simple_example.dart b/packages/go_router_builder/example/lib/simple_example.dart index 6253fc6e9d16..dee29d0c867c 100644 --- a/packages/go_router_builder/example/lib/simple_example.dart +++ b/packages/go_router_builder/example/lib/simple_example.dart @@ -28,6 +28,7 @@ class App extends StatelessWidget { @TypedGoRoute( path: '/', + name: 'Home', routes: >[ TypedGoRoute(path: 'family/:familyId') ], 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 16538ef16225..d3e624d63dea 100644 --- a/packages/go_router_builder/example/lib/simple_example.g.dart +++ b/packages/go_router_builder/example/lib/simple_example.g.dart @@ -14,6 +14,7 @@ List get $appRoutes => [ RouteBase get $homeRoute => GoRouteData.$route( path: '/', + name: 'Home', factory: $HomeRouteExtension._fromState, routes: [ GoRouteData.$route( @@ -40,7 +41,7 @@ extension $HomeRouteExtension on HomeRoute { extension $FamilyRouteExtension on FamilyRoute { static FamilyRoute _fromState(GoRouterState state) => FamilyRoute( - state.params['familyId']!, + state.pathParameters['familyId']!, ); String get location => GoRouteData.$location( diff --git a/packages/go_router_builder/example/pubspec.yaml b/packages/go_router_builder/example/pubspec.yaml index b8b7c9fa6798..766952e4ca97 100644 --- a/packages/go_router_builder/example/pubspec.yaml +++ b/packages/go_router_builder/example/pubspec.yaml @@ -3,12 +3,12 @@ description: go_router_builder examples publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=2.18.0 <4.0.0" dependencies: flutter: sdk: flutter - go_router: ^6.2.0 + go_router: ^7.0.0 provider: 6.0.5 dev_dependencies: diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index 50d7e7738a95..a67f33bab059 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -37,6 +37,7 @@ class InfoIterable extends IterableBase { class RouteConfig { RouteConfig._( this._path, + this._name, this._routeDataClass, this._parent, this._key, @@ -75,6 +76,7 @@ class RouteConfig { final bool isShellRoute = type.element.name == 'TypedShellRoute'; String? path; + String? name; if (!isShellRoute) { final ConstantReader pathValue = reader.read('path'); @@ -85,6 +87,9 @@ class RouteConfig { ); } path = pathValue.stringValue; + + final ConstantReader nameValue = reader.read('name'); + name = nameValue.isNull ? null : nameValue.stringValue; } final DartType typeParamType = type.typeArguments.single; @@ -104,6 +109,7 @@ class RouteConfig { final RouteConfig value = RouteConfig._( path ?? '', + name, classElement, parent, _generateNavigatorKeyGetterCode( @@ -121,6 +127,7 @@ class RouteConfig { final List _children = []; final String _path; + final String? _name; final InterfaceElement _routeDataClass; final RouteConfig? _parent; final String? _key; @@ -352,6 +359,7 @@ routes: [${_children.map((RouteConfig e) => '${e._routeDefinition()},').join()}] return ''' GoRouteData.\$route( path: ${escapeDartString(_path)}, + ${_name != null ? 'name: ${escapeDartString(_name!)},' : ''} factory: $_extensionName._fromState, $navigatorKey $routesBit diff --git a/packages/go_router_builder/lib/src/type_helpers.dart b/packages/go_router_builder/lib/src/type_helpers.dart index 9f51e1b15767..1ed850724651 100644 --- a/packages/go_router_builder/lib/src/type_helpers.dart +++ b/packages/go_router_builder/lib/src/type_helpers.dart @@ -98,11 +98,11 @@ String _stateValueAccess(ParameterElement element) { } if (element.isRequired) { - return 'params[${escapeDartString(element.name)}]!'; + return 'pathParameters[${escapeDartString(element.name)}]!'; } if (element.isOptional) { - return 'queryParams[${escapeDartString(element.name.kebab)}]'; + return 'queryParameters[${escapeDartString(element.name.kebab)}]'; } throw InvalidGenerationSourceError( @@ -329,7 +329,7 @@ abstract class _TypeHelperWithHelper extends _TypeHelper { if (!parameterElement.isRequired) { return '$convertMapValueHelperName(' '${escapeDartString(parameterElement.name.kebab)}, ' - 'state.queryParams, ' + 'state.queryParameters, ' '${helperName(paramType)})'; } return '${helperName(paramType)}' diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 11726b6ebcc7..a8ae680e97a5 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.2 +version: 2.0.1 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 @@ -23,6 +23,6 @@ dependencies: dev_dependencies: build_runner: ^2.0.0 - go_router: ^6.0.10 + go_router: ^7.0.0 source_gen_test: ^1.0.0 test: ^1.20.0 diff --git a/packages/go_router_builder/test/builder_test.dart b/packages/go_router_builder/test/builder_test.dart index e7d2b1440bcc..056d854a4c00 100644 --- a/packages/go_router_builder/test/builder_test.dart +++ b/packages/go_router_builder/test/builder_test.dart @@ -37,4 +37,6 @@ const Set _expectedAnnotatedTests = { 'NullableDefaultValueRoute', 'IterableWithEnumRoute', 'IterableDefaultValueRoute', + 'NamedRoute', + 'NamedEscapedRoute', }; 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 1e39c205ef6c..678a5286e088 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 @@ -70,7 +70,7 @@ RouteBase get $enumParam => GoRouteData.$route( extension $EnumParamExtension on EnumParam { static EnumParam _fromState(GoRouterState state) => EnumParam( - y: _$EnumTestEnumMap._$fromName(state.params['y']!), + y: _$EnumTestEnumMap._$fromName(state.pathParameters['y']!), ); String get location => GoRouteData.$location( @@ -119,7 +119,8 @@ RouteBase get $defaultValueRoute => GoRouteData.$route( extension $DefaultValueRouteExtension on DefaultValueRoute { static DefaultValueRoute _fromState(GoRouterState state) => DefaultValueRoute( - param: _$convertMapValue('param', state.queryParams, int.parse) ?? 0, + param: + _$convertMapValue('param', state.queryParameters, int.parse) ?? 0, ); String get location => GoRouteData.$location( @@ -160,7 +161,8 @@ RouteBase get $extraValueRoute => GoRouteData.$route( extension $ExtraValueRouteExtension on ExtraValueRoute { static ExtraValueRoute _fromState(GoRouterState state) => ExtraValueRoute( - param: _$convertMapValue('param', state.queryParams, int.parse) ?? 0, + param: + _$convertMapValue('param', state.queryParameters, int.parse) ?? 0, $extra: state.extra as int?, ); @@ -325,3 +327,54 @@ class IterableDefaultValueRoute extends GoRouteData { IterableDefaultValueRoute({this.param = const [0]}); final Iterable param; } + +@ShouldGenerate(r''' +RouteBase get $namedRoute => GoRouteData.$route( + path: '/named-route', + name: 'namedRoute', + factory: $NamedRouteExtension._fromState, + ); + +extension $NamedRouteExtension on NamedRoute { + static NamedRoute _fromState(GoRouterState state) => NamedRoute(); + + String get location => GoRouteData.$location( + '/named-route', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); +} +''') +@TypedGoRoute(path: '/named-route', name: 'namedRoute') +class NamedRoute extends GoRouteData {} + +@ShouldGenerate(r''' +RouteBase get $namedEscapedRoute => GoRouteData.$route( + path: '/named-route', + name: r'named$Route', + factory: $NamedEscapedRouteExtension._fromState, + ); + +extension $NamedEscapedRouteExtension on NamedEscapedRoute { + static NamedEscapedRoute _fromState(GoRouterState state) => + NamedEscapedRoute(); + + String get location => GoRouteData.$location( + '/named-route', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); +} +''') +@TypedGoRoute(path: '/named-route', name: r'named$Route') +class NamedEscapedRoute extends GoRouteData {} diff --git a/packages/google_identity_services_web/CHANGELOG.md b/packages/google_identity_services_web/CHANGELOG.md index 966e8125719c..4ece505495b4 100644 --- a/packages/google_identity_services_web/CHANGELOG.md +++ b/packages/google_identity_services_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 0.2.1 * Relaxes the `renderButton` API so any JS-Interop Object can be its `target`. diff --git a/packages/google_identity_services_web/example/pubspec.yaml b/packages/google_identity_services_web/example/pubspec.yaml index b305694d5c74..3244341b54f4 100644 --- a/packages/google_identity_services_web/example/pubspec.yaml +++ b/packages/google_identity_services_web/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: 'none' version: 0.0.1 environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=2.18.0 <4.0.0" dependencies: flutter: diff --git a/packages/google_identity_services_web/pubspec.yaml b/packages/google_identity_services_web/pubspec.yaml index 8d4b6c53eb57..47d69e0fe352 100644 --- a/packages/google_identity_services_web/pubspec.yaml +++ b/packages/google_identity_services_web/pubspec.yaml @@ -5,7 +5,7 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 0.2.1 environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=2.18.0 <4.0.0" dependencies: js: ^0.6.4 diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index cf1cdd9a3df0..b3ccb3741d8d 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,6 +1,12 @@ -## NEXT +## 2.2.7 + +* Removes obsolete null checks on non-nullable values. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 2.2.6 * Aligns Dart and Flutter SDK constraints. +* Updates README.md to specify not to use GoogleMaps widget within an unbounded widget. ## 2.2.5 diff --git a/packages/google_maps_flutter/google_maps_flutter/README.md b/packages/google_maps_flutter/google_maps_flutter/README.md index 820e0de42aa1..7a97d99d4470 100644 --- a/packages/google_maps_flutter/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/google_maps_flutter/README.md @@ -105,6 +105,9 @@ You can now add a `GoogleMap` widget to your widget tree. The map view can be controlled with the `GoogleMapController` that is passed to the `GoogleMap`'s `onMapCreated` callback. +The `GoogleMap` widget should be used within a widget with a bounded size. Using it +in an unbounded widget will cause the application to throw a Flutter exception. + ### Sample Usage diff --git a/packages/google_maps_flutter/google_maps_flutter/example/.pluginToolsConfig.yaml b/packages/google_maps_flutter/google_maps_flutter/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml index da09f88a355f..9c0ab999ddbc 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -3,8 +3,8 @@ 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" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: cupertino_icons: ^1.0.5 diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart index cd3d0781e471..dedb8400a32e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart @@ -27,7 +27,6 @@ class GoogleMapController { CameraPosition initialCameraPosition, _GoogleMapState googleMapState, ) async { - assert(id != null); await GoogleMapsFlutterPlatform.instance.init(id); return GoogleMapController._( googleMapState, @@ -100,7 +99,6 @@ class GoogleMapController { /// /// The returned [Future] completes after listeners have been notified. Future _updateMarkers(MarkerUpdates markerUpdates) { - assert(markerUpdates != null); return GoogleMapsFlutterPlatform.instance .updateMarkers(markerUpdates, mapId: mapId); } @@ -112,7 +110,6 @@ class GoogleMapController { /// /// The returned [Future] completes after listeners have been notified. Future _updatePolygons(PolygonUpdates polygonUpdates) { - assert(polygonUpdates != null); return GoogleMapsFlutterPlatform.instance .updatePolygons(polygonUpdates, mapId: mapId); } @@ -124,7 +121,6 @@ class GoogleMapController { /// /// The returned [Future] completes after listeners have been notified. Future _updatePolylines(PolylineUpdates polylineUpdates) { - assert(polylineUpdates != null); return GoogleMapsFlutterPlatform.instance .updatePolylines(polylineUpdates, mapId: mapId); } @@ -136,7 +132,6 @@ class GoogleMapController { /// /// The returned [Future] completes after listeners have been notified. Future _updateCircles(CircleUpdates circleUpdates) { - assert(circleUpdates != null); return GoogleMapsFlutterPlatform.instance .updateCircles(circleUpdates, mapId: mapId); } @@ -160,7 +155,6 @@ class GoogleMapController { /// in-memory cache of tiles. If you want to cache tiles for longer, you /// should implement an on-disk cache. Future clearTileCache(TileOverlayId tileOverlayId) async { - assert(tileOverlayId != null); return GoogleMapsFlutterPlatform.instance .clearTileCache(tileOverlayId, mapId: mapId); } @@ -234,7 +228,6 @@ class GoogleMapController { /// * [hideMarkerInfoWindow] to hide the Info Window. /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. Future showMarkerInfoWindow(MarkerId markerId) { - assert(markerId != null); return GoogleMapsFlutterPlatform.instance .showMarkerInfoWindow(markerId, mapId: mapId); } @@ -248,7 +241,6 @@ class GoogleMapController { /// * [showMarkerInfoWindow] to show the Info Window. /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. Future hideMarkerInfoWindow(MarkerId markerId) { - assert(markerId != null); return GoogleMapsFlutterPlatform.instance .hideMarkerInfoWindow(markerId, mapId: mapId); } @@ -262,7 +254,6 @@ class GoogleMapController { /// * [showMarkerInfoWindow] to show the Info Window. /// * [hideMarkerInfoWindow] to hide the Info Window. Future isMarkerInfoWindowShown(MarkerId markerId) { - assert(markerId != null); return GoogleMapsFlutterPlatform.instance .isMarkerInfoWindowShown(markerId, mapId: mapId); } diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart index 15ea6182d7b2..a081eab75859 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart @@ -123,7 +123,7 @@ class GoogleMap extends StatefulWidget { this.onCameraIdle, this.onTap, this.onLongPress, - }) : assert(initialCameraPosition != null); + }); /// Callback method for when the map is ready to be used. /// @@ -418,7 +418,6 @@ class _GoogleMapState extends State { } void onMarkerTap(MarkerId markerId) { - assert(markerId != null); final Marker? marker = _markers[markerId]; if (marker == null) { throw UnknownMapObjectIdError('marker', markerId, 'onTap'); @@ -430,7 +429,6 @@ class _GoogleMapState extends State { } void onMarkerDragStart(MarkerId markerId, LatLng position) { - assert(markerId != null); final Marker? marker = _markers[markerId]; if (marker == null) { throw UnknownMapObjectIdError('marker', markerId, 'onDragStart'); @@ -442,7 +440,6 @@ class _GoogleMapState extends State { } void onMarkerDrag(MarkerId markerId, LatLng position) { - assert(markerId != null); final Marker? marker = _markers[markerId]; if (marker == null) { throw UnknownMapObjectIdError('marker', markerId, 'onDrag'); @@ -454,7 +451,6 @@ class _GoogleMapState extends State { } void onMarkerDragEnd(MarkerId markerId, LatLng position) { - assert(markerId != null); final Marker? marker = _markers[markerId]; if (marker == null) { throw UnknownMapObjectIdError('marker', markerId, 'onDragEnd'); @@ -466,7 +462,6 @@ class _GoogleMapState extends State { } void onPolygonTap(PolygonId polygonId) { - assert(polygonId != null); final Polygon? polygon = _polygons[polygonId]; if (polygon == null) { throw UnknownMapObjectIdError('polygon', polygonId, 'onTap'); @@ -478,7 +473,6 @@ class _GoogleMapState extends State { } void onPolylineTap(PolylineId polylineId) { - assert(polylineId != null); final Polyline? polyline = _polylines[polylineId]; if (polyline == null) { throw UnknownMapObjectIdError('polyline', polylineId, 'onTap'); @@ -490,7 +484,6 @@ class _GoogleMapState extends State { } void onCircleTap(CircleId circleId) { - assert(circleId != null); final Circle? circle = _circles[circleId]; if (circle == null) { throw UnknownMapObjectIdError('marker', circleId, 'onTap'); @@ -502,7 +495,6 @@ class _GoogleMapState extends State { } void onInfoWindowTap(MarkerId markerId) { - assert(markerId != null); final Marker? marker = _markers[markerId]; if (marker == null) { throw UnknownMapObjectIdError('marker', markerId, 'InfoWindow onTap'); @@ -514,7 +506,6 @@ class _GoogleMapState extends State { } void onTap(LatLng position) { - assert(position != null); final ArgumentCallback? onTap = widget.onTap; if (onTap != null) { onTap(position); @@ -522,7 +513,6 @@ class _GoogleMapState extends State { } void onLongPress(LatLng position) { - assert(position != null); final ArgumentCallback? onLongPress = widget.onLongPress; if (onLongPress != null) { onLongPress(position); diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index ba673ccc4e4f..48b156256899 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -2,11 +2,11 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.2.5 +version: 2.2.7 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart index 2c6aba1bb0ba..c28ff1f4f55f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart @@ -293,9 +293,6 @@ class FakePlatformGoogleMap { } void updateTileOverlays(Map updateTileOverlayUpdates) { - if (updateTileOverlayUpdates == null) { - return; - } final List>? tileOverlaysToAddList = updateTileOverlayUpdates['tileOverlaysToAdd'] != null ? List.castFrom>( 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 2e5b776db12e..7b054ced1270 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,8 @@ +## 2.4.15 + +* Removes obsolete null checks on non-nullable values. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.4.14 * Updates gradle, AGP and fixes some lint errors. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/.pluginToolsConfig.yaml b/packages/google_maps_flutter/google_maps_flutter_android/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml index 259462c14656..2fafe6eacd9b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml @@ -3,8 +3,8 @@ 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" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: cupertino_icons: ^1.0.5 diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart index e072952c3673..132cd4905bea 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart @@ -307,7 +307,6 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { Map optionsUpdate, { required int mapId, }) { - assert(optionsUpdate != null); return _channel(mapId).invokeMethod( 'map#update', { @@ -321,7 +320,6 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { MarkerUpdates markerUpdates, { required int mapId, }) { - assert(markerUpdates != null); return _channel(mapId).invokeMethod( 'markers#update', markerUpdates.toJson(), @@ -333,7 +331,6 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { PolygonUpdates polygonUpdates, { required int mapId, }) { - assert(polygonUpdates != null); return _channel(mapId).invokeMethod( 'polygons#update', polygonUpdates.toJson(), @@ -345,7 +342,6 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { PolylineUpdates polylineUpdates, { required int mapId, }) { - assert(polylineUpdates != null); return _channel(mapId).invokeMethod( 'polylines#update', polylineUpdates.toJson(), @@ -357,7 +353,6 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { CircleUpdates circleUpdates, { required int mapId, }) { - assert(circleUpdates != null); return _channel(mapId).invokeMethod( 'circles#update', circleUpdates.toJson(), @@ -468,7 +463,6 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { MarkerId markerId, { required int mapId, }) { - assert(markerId != null); return _channel(mapId).invokeMethod( 'markers#showInfoWindow', {'markerId': markerId.value}); } @@ -478,7 +472,6 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { MarkerId markerId, { required int mapId, }) { - assert(markerId != null); return _channel(mapId).invokeMethod( 'markers#hideInfoWindow', {'markerId': markerId.value}); } @@ -488,7 +481,6 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { MarkerId markerId, { required int mapId, }) async { - assert(markerId != null); return (await _channel(mapId).invokeMethod( 'markers#isInfoWindowShown', {'markerId': markerId.value}))!; 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 931f70b1ccdf..04c9f12bbbaa 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -2,11 +2,11 @@ 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.14 +version: 2.4.15 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: 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 47ef18480467..703010222f34 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.3 + +* Removes obsolete null checks on non-nullable values. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.2.2 * Sets an upper bound on the `GoogleMaps` SDK version that can be used, to diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/pubspec.yaml index f34109a26fd3..f9a7ee1dc474 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/pubspec.yaml @@ -3,8 +3,8 @@ 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" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: cupertino_icons: ^1.0.5 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 index f34109a26fd3..f9a7ee1dc474 100644 --- 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 @@ -3,8 +3,8 @@ 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" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: cupertino_icons: ^1.0.5 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 index f34109a26fd3..f9a7ee1dc474 100644 --- 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 @@ -3,8 +3,8 @@ 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" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: cupertino_icons: ^1.0.5 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 index 29640b926d8c..e20bdb9a5af2 100644 --- 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 @@ -3,8 +3,8 @@ description: Shared Dart code for the example apps. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: cupertino_icons: ^1.0.5 diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart index affd706e022b..b1ecca20817a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart @@ -289,7 +289,6 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { Map optionsUpdate, { required int mapId, }) { - assert(optionsUpdate != null); return _channel(mapId).invokeMethod( 'map#update', { @@ -303,7 +302,6 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { MarkerUpdates markerUpdates, { required int mapId, }) { - assert(markerUpdates != null); return _channel(mapId).invokeMethod( 'markers#update', markerUpdates.toJson(), @@ -315,7 +313,6 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { PolygonUpdates polygonUpdates, { required int mapId, }) { - assert(polygonUpdates != null); return _channel(mapId).invokeMethod( 'polygons#update', polygonUpdates.toJson(), @@ -327,7 +324,6 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { PolylineUpdates polylineUpdates, { required int mapId, }) { - assert(polylineUpdates != null); return _channel(mapId).invokeMethod( 'polylines#update', polylineUpdates.toJson(), @@ -339,7 +335,6 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { CircleUpdates circleUpdates, { required int mapId, }) { - assert(circleUpdates != null); return _channel(mapId).invokeMethod( 'circles#update', circleUpdates.toJson(), @@ -450,7 +445,6 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { MarkerId markerId, { required int mapId, }) { - assert(markerId != null); return _channel(mapId).invokeMethod( 'markers#showInfoWindow', {'markerId': markerId.value}); } @@ -460,7 +454,6 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { MarkerId markerId, { required int mapId, }) { - assert(markerId != null); return _channel(mapId).invokeMethod( 'markers#hideInfoWindow', {'markerId': markerId.value}); } @@ -470,7 +463,6 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { MarkerId markerId, { required int mapId, }) async { - assert(markerId != null); return (await _channel(mapId).invokeMethod( 'markers#isInfoWindowShown', {'markerId': markerId.value}))!; 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 f24935c9105f..499387f64336 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.2 +version: 2.2.3 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md index 0fb0375c0d97..c93d3e2fa5bc 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.2.7 +* Removes obsolete null checks on non-nullable values. * Updates minimum Flutter version to 3.3. * Aligns Dart and Flutter SDK constraints. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart index 3fd860e126eb..dc55e7443276 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart @@ -292,7 +292,6 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { Map optionsUpdate, { required int mapId, }) { - assert(optionsUpdate != null); return channel(mapId).invokeMethod( 'map#update', { @@ -306,7 +305,6 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { MarkerUpdates markerUpdates, { required int mapId, }) { - assert(markerUpdates != null); return channel(mapId).invokeMethod( 'markers#update', markerUpdates.toJson(), @@ -318,7 +316,6 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { PolygonUpdates polygonUpdates, { required int mapId, }) { - assert(polygonUpdates != null); return channel(mapId).invokeMethod( 'polygons#update', polygonUpdates.toJson(), @@ -330,7 +327,6 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { PolylineUpdates polylineUpdates, { required int mapId, }) { - assert(polylineUpdates != null); return channel(mapId).invokeMethod( 'polylines#update', polylineUpdates.toJson(), @@ -342,7 +338,6 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { CircleUpdates circleUpdates, { required int mapId, }) { - assert(circleUpdates != null); return channel(mapId).invokeMethod( 'circles#update', circleUpdates.toJson(), @@ -452,7 +447,6 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { MarkerId markerId, { required int mapId, }) { - assert(markerId != null); return channel(mapId).invokeMethod( 'markers#showInfoWindow', {'markerId': markerId.value}); } @@ -462,7 +456,6 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { MarkerId markerId, { required int mapId, }) { - assert(markerId != null); return channel(mapId).invokeMethod( 'markers#hideInfoWindow', {'markerId': markerId.value}); } @@ -472,7 +465,6 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { MarkerId markerId, { required int mapId, }) async { - assert(markerId != null); return (await channel(mapId).invokeMethod('markers#isInfoWindowShown', {'markerId': markerId.value}))!; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/callbacks.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/callbacks.dart index 5d6af90290e0..41f0fe23ef14 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/callbacks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/callbacks.dart @@ -44,7 +44,6 @@ class ArgumentCallbacks { /// Adds a callback to this collection. void add(ArgumentCallback callback) { - assert(callback != null); _callbacks.add(callback); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart index 6d1ce164238b..01126afb869e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart @@ -23,10 +23,7 @@ class CameraPosition { required this.target, this.tilt = 0.0, this.zoom = 0.0, - }) : assert(bearing != null), - assert(target != null), - assert(tilt != null), - assert(zoom != null); + }); /// The camera's bearing in degrees, measured clockwise from north. /// diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart index 5bef7baf0bf4..3ca79583d12a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart @@ -43,7 +43,6 @@ class Cap { BitmapDescriptor bitmapDescriptor, { double refWidth = 10, }) { - assert(bitmapDescriptor != null); assert(refWidth > 0.0); return Cap._(['customCap', bitmapDescriptor.toJson(), refWidth]); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart index 81fe08bb1329..b00446a542b4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart @@ -16,9 +16,7 @@ class LatLng { /// The longitude is normalized to the half-open interval from -180.0 /// (inclusive) to +180.0 (exclusive). const LatLng(double latitude, double longitude) - : assert(latitude != null), - assert(longitude != null), - latitude = + : latitude = latitude < -90.0 ? -90.0 : (90.0 < latitude ? 90.0 : latitude), // Avoids normalization if possible to prevent unnecessary loss of precision longitude = longitude >= -180 && longitude < 180 @@ -76,9 +74,7 @@ class LatLngBounds { /// The latitude of the southwest corner cannot be larger than the /// latitude of the northeast corner. LatLngBounds({required this.southwest, required this.northeast}) - : assert(southwest != null), - assert(northeast != null), - assert(southwest.latitude <= northeast.latitude); + : assert(southwest.latitude <= northeast.latitude); /// The southwest corner of the rectangle. final LatLng southwest; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart index 953746daa745..2f2216c1b330 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart @@ -13,7 +13,7 @@ class MapsObjectId { /// Creates an immutable object representing a [T] among [GoogleMap] Ts. /// /// An [AssertionError] will be thrown if [value] is null. - const MapsObjectId(this.value) : assert(value != null); + const MapsObjectId(this.value); /// The value of the id. final String value; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart index da3f89df9f71..cd68b53b58ca 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart @@ -155,7 +155,7 @@ class Marker implements MapsObject { this.onDrag, this.onDragStart, this.onDragEnd, - }) : assert(alpha == null || (0.0 <= alpha && alpha <= 1.0)); + }) : assert(0.0 <= alpha && alpha <= 1.0); /// Uniquely identifies a [Marker]. final MarkerId markerId; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart index ce78d3307155..8298b90ae947 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart @@ -148,13 +148,9 @@ class Polygon implements MapsObject { addIfPresent('visible', visible); addIfPresent('zIndex', zIndex); - if (points != null) { - json['points'] = _pointsToJson(); - } + json['points'] = _pointsToJson(); - if (holes != null) { - json['holes'] = _holesToJson(); - } + json['holes'] = _holesToJson(); return json; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart index cd0a64ee7b82..b7d50cd3962d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart @@ -180,13 +180,9 @@ class Polyline implements MapsObject { addIfPresent('width', width); addIfPresent('zIndex', zIndex); - if (points != null) { - json['points'] = _pointsToJson(); - } + json['points'] = _pointsToJson(); - if (patterns != null) { - json['pattern'] = _patternToJson(); - } + json['pattern'] = _patternToJson(); return json; } @@ -228,9 +224,7 @@ class Polyline implements MapsObject { Object _patternToJson() { final List result = []; for (final PatternItem patternItem in patterns) { - if (patternItem != null) { - result.add(patternItem.toJson()); - } + result.add(patternItem.toJson()); } return result; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml index 879604908815..20abc39b27e3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/google_maps_f issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%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.6 +version: 2.2.7 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 077d012e5b8b..b42ce52c3d99 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.0+9 + +* Removes obsolete null checks on non-nullable values. + ## 0.4.0+8 * Updates minimum Flutter version to 3.3. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart index bc6eac14200f..e307941887a4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart @@ -30,10 +30,6 @@ class CirclesController extends GeometryController { } void _addCircle(Circle circle) { - if (circle == null) { - return; - } - final gmaps.CircleOptions circleOptions = _circleOptionsFromCircle(circle); final gmaps.Circle gmCircle = gmaps.Circle(circleOptions)..map = googleMap; final CircleController controller = CircleController( 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 dcce8d35699e..3840ef21cc99 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 @@ -9,23 +9,13 @@ final gmaps.LatLng _nullGmapsLatLng = gmaps.LatLng(0, 0); final gmaps.LatLngBounds _nullGmapsLatLngBounds = gmaps.LatLngBounds(_nullGmapsLatLng, _nullGmapsLatLng); -// Defaults taken from the Google Maps Platform SDK documentation. -const String _defaultCssColor = '#000000'; -const double _defaultCssOpacity = 0.0; - // Converts a [Color] into a valid CSS value #RRGGBB. String _getCssColor(Color color) { - if (color == null) { - return _defaultCssColor; - } return '#${color.value.toRadixString(16).padLeft(8, '0').substring(2)}'; } // Extracts the opacity from a [Color]. double _getCssOpacity(Color color) { - if (color == null) { - return _defaultCssOpacity; - } return color.opacity; } @@ -114,11 +104,9 @@ gmaps.MapOptions _applyInitialPosition( gmaps.MapOptions options, ) { // Adjust the initial position, if passed... - if (initialPosition != null) { - options.zoom = initialPosition.zoom; - options.center = gmaps.LatLng( - initialPosition.target.latitude, initialPosition.target.longitude); - } + options.zoom = initialPosition.zoom; + options.center = gmaps.LatLng( + initialPosition.target.latitude, initialPosition.target.longitude); return options; } @@ -255,33 +243,31 @@ gmaps.Icon? _gmIconFromBitmapDescriptor(BitmapDescriptor bitmapDescriptor) { gmaps.Icon? icon; - if (iconConfig != null) { - if (iconConfig[0] == 'fromAssetImage') { - assert(iconConfig.length >= 2); - // iconConfig[2] contains the DPIs of the screen, but that information is - // already encoded in the iconConfig[1] - icon = gmaps.Icon() - ..url = ui.webOnlyAssetManager.getAssetUrl(iconConfig[1]! as String); - - final gmaps.Size? size = _gmSizeFromIconConfig(iconConfig, 3); - if (size != null) { - icon - ..size = size - ..scaledSize = size; - } - } else if (iconConfig[0] == 'fromBytes') { - // Grab the bytes, and put them into a blob - final List bytes = iconConfig[1]! as List; - // Create a Blob from bytes, but let the browser figure out the encoding - final Blob blob = Blob([bytes]); - icon = gmaps.Icon()..url = Url.createObjectUrlFromBlob(blob); - - final gmaps.Size? size = _gmSizeFromIconConfig(iconConfig, 2); - if (size != null) { - icon - ..size = size - ..scaledSize = size; - } + if (iconConfig[0] == 'fromAssetImage') { + assert(iconConfig.length >= 2); + // iconConfig[2] contains the DPIs of the screen, but that information is + // already encoded in the iconConfig[1] + icon = gmaps.Icon() + ..url = ui.webOnlyAssetManager.getAssetUrl(iconConfig[1]! as String); + + final gmaps.Size? size = _gmSizeFromIconConfig(iconConfig, 3); + if (size != null) { + icon + ..size = size + ..scaledSize = size; + } + } else if (iconConfig[0] == 'fromBytes') { + // Grab the bytes, and put them into a blob + final List bytes = iconConfig[1]! as List; + // Create a Blob from bytes, but let the browser figure out the encoding + final Blob blob = Blob([bytes]); + icon = gmaps.Icon()..url = Url.createObjectUrlFromBlob(blob); + + final gmaps.Size? size = _gmSizeFromIconConfig(iconConfig, 2); + if (size != null) { + icon + ..size = size + ..scaledSize = size; } } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart index c2085a2bddfc..b64f99501fcd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart @@ -39,7 +39,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { // The internal instance of our controller is initialized eagerly in `buildView`, // so we don't have to do anything in this method, which is left intentionally // blank. - assert(_map(mapId) != null, 'Must call buildWidget before init!'); + assert(_mapById[mapId] != null, 'Must call buildWidget before init!'); } /// Updates the options of a given `mapId`. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart index 9d607e9bbc6a..50b4bd3407ea 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart @@ -25,25 +25,19 @@ class MarkerController { } if (onDragStart != null) { marker.onDragstart.listen((gmaps.MapMouseEvent event) { - if (marker != null) { - marker.position = event.latLng; - } + marker.position = event.latLng; onDragStart.call(event.latLng ?? _nullGmapsLatLng); }); } if (onDrag != null) { marker.onDrag.listen((gmaps.MapMouseEvent event) { - if (marker != null) { - marker.position = event.latLng; - } + marker.position = event.latLng; onDrag.call(event.latLng ?? _nullGmapsLatLng); }); } if (onDragEnd != null) { marker.onDragend.listen((gmaps.MapMouseEvent event) { - if (marker != null) { - marker.position = event.latLng; - } + marker.position = event.latLng; onDragEnd.call(event.latLng ?? _nullGmapsLatLng); }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart index 1a712b109677..0c378f9daba3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart @@ -30,10 +30,6 @@ class MarkersController extends GeometryController { } void _addMarker(Marker marker) { - if (marker == null) { - return; - } - final gmaps.InfoWindowOptions? infoWindowOptions = _infoWindowOptionsFromMarker(marker); gmaps.InfoWindow? gmInfoWindow; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart index 12e378cfc59c..412fd156524b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart @@ -26,16 +26,10 @@ class PolygonsController extends GeometryController { /// /// Wraps each Polygon into its corresponding [PolygonController]. void addPolygons(Set polygonsToAdd) { - if (polygonsToAdd != null) { - polygonsToAdd.forEach(_addPolygon); - } + polygonsToAdd.forEach(_addPolygon); } void _addPolygon(Polygon polygon) { - if (polygon == null) { - return; - } - final gmaps.PolygonOptions polygonOptions = _polygonOptionsFromPolygon(googleMap, polygon); final gmaps.Polygon gmPolygon = gmaps.Polygon(polygonOptions) @@ -51,9 +45,7 @@ class PolygonsController extends GeometryController { /// Updates a set of [Polygon] objects with new options. void changePolygons(Set polygonsToChange) { - if (polygonsToChange != null) { - polygonsToChange.forEach(_changePolygon); - } + polygonsToChange.forEach(_changePolygon); } void _changePolygon(Polygon polygon) { diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart index 2d3f1618b42c..0384ea16cc4b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart @@ -30,10 +30,6 @@ class PolylinesController extends GeometryController { } void _addPolyline(Polyline polyline) { - if (polyline == null) { - return; - } - final gmaps.PolylineOptions polylineOptions = _polylineOptionsFromPolyline(googleMap, polyline); final gmaps.Polyline gmPolyline = gmaps.Polyline(polylineOptions) 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 67df16f00167..c88b2aefaba5 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+8 +version: 0.4.0+9 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 2b728ba9cb4c..1751c5cbfc99 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.1.1 + +* Removes obsolete null checks on non-nullable values. + ## 6.1.0 * Exposes the new method `canAccessScopes`. diff --git a/packages/google_sign_in/google_sign_in/example/.pluginToolsConfig.yaml b/packages/google_sign_in/google_sign_in/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index f3afb11c8e93..4af1229882c5 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -48,9 +48,7 @@ class GoogleSignInAccount implements GoogleIdentity { id = data.id, photoUrl = data.photoUrl, serverAuthCode = data.serverAuthCode, - _idToken = data.idToken { - assert(id != null); - } + _idToken = data.idToken; // These error codes must match with ones declared on Android and iOS sides. diff --git a/packages/google_sign_in/google_sign_in/lib/widgets.dart b/packages/google_sign_in/google_sign_in/lib/widgets.dart index 8d67e60739b3..ab04de4319d5 100644 --- a/packages/google_sign_in/google_sign_in/lib/widgets.dart +++ b/packages/google_sign_in/google_sign_in/lib/widgets.dart @@ -27,7 +27,7 @@ class GoogleUserCircleAvatar extends StatelessWidget { this.placeholderPhotoUrl, this.foregroundColor, this.backgroundColor, - }) : assert(identity != null); + }); /// A regular expression that matches against the "size directive" path /// segment of Google profile image URLs. diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index f2b7bdd1aa8e..3ac3bf5aa796 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 6.1.0 +version: 6.1.1 environment: sdk: ">=2.18.0 <4.0.0" 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 7aeafc1a0f01..dbb6a8ab0a45 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,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 6.1.14 * Fixes compatibility with AGP versions older than 4.2. diff --git a/packages/google_sign_in/google_sign_in_android/example/.pluginToolsConfig.yaml b/packages/google_sign_in/google_sign_in_android/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml index 1887a2f3a1eb..630686405581 100644 --- a/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Example of Google Sign-In plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: 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 73904741a401..516f09cc3b57 100644 --- a/packages/google_sign_in/google_sign_in_android/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 6.1.14 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md index 82cb24bd5658..566a29a36c91 100644 --- a/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 5.6.2 + +* Updates functions without a prototype to avoid deprecation warning. + ## 5.6.1 * Clarifies explanation of endorsement in README. diff --git a/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml index c06729128084..ba9c8b1bbd3a 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Example of Google Sign-In plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.m b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.m index 7beb604aaee3..069928422863 100644 --- a/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.m +++ b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.m @@ -14,7 +14,7 @@ static NSString *const kServerClientIdKey = @"SERVER_CLIENT_ID"; -static NSDictionary *loadGoogleServiceInfo() { +static NSDictionary *loadGoogleServiceInfo(void) { NSString *plistPath = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"]; if (plistPath) { diff --git a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml index 50c33f80aa1f..6ce44b89b533 100644 --- a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: google_sign_in_ios description: iOS implementation of the google_sign_in plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.6.1 +version: 5.6.2 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart index 72197f092120..b436a983fc70 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart @@ -47,13 +47,10 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { /// For tests, the plugin can skip its loading process with [debugOverrideLoader], /// and the implementation of the underlying GIS SDK client through [debugOverrideGisSdkClient]. GoogleSignInPlugin({ + @visibleForTesting bool debugOverrideLoader = false, + @visibleForTesting GisSdkClient? debugOverrideGisSdkClient, @visibleForTesting - bool debugOverrideLoader = false, - @visibleForTesting - GisSdkClient? debugOverrideGisSdkClient, - @visibleForTesting - StreamController? - debugOverrideUserDataController, + StreamController? debugOverrideUserDataController, }) : _gisSdkClient = debugOverrideGisSdkClient, _userDataController = debugOverrideUserDataController ?? StreamController.broadcast() { diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 117f6af6cff5..f1e09f50b51b 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.7+5 + +* Fixes `BuildContext` handling in example. + ## 0.8.7+4 * Updates README to mention usage of `launchMode: singleInstance` for Android. diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 247c0a937a85..4a96ce194ef9 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -81,51 +81,53 @@ class _MyHomePageState extends State { } Future _onImageButtonPressed(ImageSource source, - {BuildContext? context, bool isMultiImage = false}) async { + {required BuildContext context, bool isMultiImage = false}) async { if (_controller != null) { await _controller!.setVolume(0.0); } - if (isVideo) { - final XFile? file = await _picker.pickVideo( - source: source, maxDuration: const Duration(seconds: 10)); - await _playVideo(file); - } else if (isMultiImage) { - await _displayPickImageDialog(context!, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final List pickedFileList = await _picker.pickMultiImage( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); - setState(() { - _imageFileList = pickedFileList; - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } else { - await _displayPickImageDialog(context!, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final XFile? pickedFile = await _picker.pickImage( - source: source, - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); + if (context.mounted) { + if (isVideo) { + final XFile? file = await _picker.pickVideo( + source: source, maxDuration: const Duration(seconds: 10)); + await _playVideo(file); + } else if (isMultiImage) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = await _picker.pickMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _imageFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } } } @@ -332,7 +334,7 @@ class _MyHomePageState extends State { backgroundColor: Colors.red, onPressed: () { isVideo = true; - _onImageButtonPressed(ImageSource.gallery); + _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'video0', tooltip: 'Pick Video from gallery', @@ -345,7 +347,7 @@ class _MyHomePageState extends State { backgroundColor: Colors.red, onPressed: () { isVideo = true; - _onImageButtonPressed(ImageSource.camera); + _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'video1', tooltip: 'Take a Video', diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index c1bb2225d429..530cf5a7703b 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+4 +version: 0.8.7+5 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/image_picker/image_picker_android/AUTHORS b/packages/image_picker/image_picker_android/AUTHORS index 493a0b4ef9c2..57d4f75a1d35 100644 --- a/packages/image_picker/image_picker_android/AUTHORS +++ b/packages/image_picker/image_picker_android/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +André Sousa diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index 36e0b191d0fd..385700b3d47b 100644 --- a/packages/image_picker/image_picker_android/CHANGELOG.md +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -1,5 +1,26 @@ -## NEXT +## 0.8.6+16 +* Fixes crashes caused by `SecurityException` when calling `getPathFromUri()`. + +## 0.8.6+15 + +* Bumps androidx.activity:activity from 1.6.1 to 1.7.0. + +## 0.8.6+14 + +* Fixes Java warnings. + +## 0.8.6+13 + +* Fixes `BuildContext` handling in example. + +## 0.8.6+12 + +* Improves image resizing performance by decoding Bitmap only when needed. + +## 0.8.6+11 + +* Updates gradle to 7.6.1. * Updates gradle, AGP and fixes some lint errors. ## 0.8.6+10 diff --git a/packages/image_picker/image_picker_android/android/build.gradle b/packages/image_picker/image_picker_android/android/build.gradle index 08b4bb3b4d05..c579c848639d 100644 --- a/packages/image_picker/image_picker_android/android/build.gradle +++ b/packages/image_picker/image_picker_android/android/build.gradle @@ -36,13 +36,15 @@ android { checkAllWarnings true warningsAsErrors true disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' - baseline file("lint-baseline.xml") } dependencies { implementation 'androidx.core:core:1.9.0' implementation 'androidx.annotation:annotation:1.3.0' implementation 'androidx.exifinterface:exifinterface:1.3.6' - implementation 'androidx.activity:activity:1.6.1' + implementation 'androidx.activity:activity:1.7.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.10")) testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:5.1.1' diff --git a/packages/image_picker/image_picker_android/android/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker_android/android/gradle/wrapper/gradle-wrapper.properties index 41dfb87909a8..774fae87671b 100644 --- a/packages/image_picker/image_picker_android/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/image_picker/image_picker_android/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.4-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/image_picker/image_picker_android/android/lint-baseline.xml b/packages/image_picker/image_picker_android/android/lint-baseline.xml deleted file mode 100644 index 765a6ca914b7..000000000000 --- a/packages/image_picker/image_picker_android/android/lint-baseline.xml +++ /dev/null @@ -1,400 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java index 4ca970a3344a..df229d8cefbf 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java @@ -79,6 +79,13 @@ String getPathFromUri(final Context context, final Uri uri) { // target file was written in full. Flushing the stream merely moves // the bytes into the OS, not necessarily to the file. return null; + } catch (SecurityException e) { + // Calling `ContentResolver#openInputStream()` has been reported to throw a + // `SecurityException` on some devices in certain circumstances. Instead of crashing, we + // return `null`. + // + // See https://github.com/flutter/flutter/issues/100025 for more details. + return null; } } 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 6aa1181ce908..423088590531 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 @@ -95,7 +95,7 @@ private static class PendingCallState { public final @Nullable VideoSelectionOptions videoOptions; public final @NonNull Messages.Result> result; - private PendingCallState( + PendingCallState( @Nullable ImageSelectionOptions imageOptions, @Nullable VideoSelectionOptions videoOptions, @NonNull Messages.Result> result) { @@ -107,10 +107,10 @@ private PendingCallState( @VisibleForTesting final String fileProviderName; - private final Activity activity; - @VisibleForTesting final File externalFilesDirectory; - private final ImageResizer imageResizer; - private final ImagePickerCache cache; + private final @NonNull Activity activity; + @VisibleForTesting final @NonNull File externalFilesDirectory; + private final @NonNull ImageResizer imageResizer; + private final @NonNull ImagePickerCache cache; private final PermissionManager permissionManager; private final FileUriResolver fileUriResolver; private final FileUtils fileUtils; @@ -140,10 +140,10 @@ interface OnPathReadyListener { private final Object pendingCallStateLock = new Object(); public ImagePickerDelegate( - final Activity activity, - final File externalFilesDirectory, - final ImageResizer imageResizer, - final ImagePickerCache cache) { + final @NonNull Activity activity, + final @NonNull File externalFilesDirectory, + final @NonNull ImageResizer imageResizer, + final @NonNull ImagePickerCache cache) { this( activity, externalFilesDirectory, @@ -181,12 +181,7 @@ public void getFullImagePath(final Uri imageUri, final OnPathReadyListener liste activity, new String[] {(imageUri != null) ? imageUri.getPath() : ""}, null, - new MediaScannerConnection.OnScanCompletedListener() { - @Override - public void onScanCompleted(String path, Uri uri) { - listener.onPathReady(path); - } - }); + (path, uri) -> listener.onPathReady(path)); } }, new FileUtils(), @@ -199,13 +194,13 @@ public void onScanCompleted(String path, Uri uri) { */ @VisibleForTesting ImagePickerDelegate( - final Activity activity, - final File externalFilesDirectory, - final ImageResizer imageResizer, + final @NonNull Activity activity, + final @NonNull File externalFilesDirectory, + final @NonNull ImageResizer imageResizer, final @Nullable ImageSelectionOptions pendingImageOptions, final @Nullable VideoSelectionOptions pendingVideoOptions, final @Nullable Messages.Result> result, - final ImagePickerCache cache, + final @NonNull ImagePickerCache cache, final PermissionManager permissionManager, final FileUriResolver fileUriResolver, final FileUtils fileUtils, @@ -290,7 +285,9 @@ Messages.CacheRetrievalResult retrieveLostImage() { } public void chooseVideoFromGallery( - VideoSelectionOptions options, boolean usePhotoPicker, Messages.Result> result) { + @NonNull VideoSelectionOptions options, + boolean usePhotoPicker, + @NonNull Messages.Result> result) { if (!setPendingOptionsAndResult(null, options, result)) { finishWithAlreadyActiveError(result); return; @@ -318,7 +315,7 @@ private void launchPickVideoFromGalleryIntent(Boolean useAndroidPhotoPicker) { } public void takeVideoWithCamera( - VideoSelectionOptions options, Messages.Result> result) { + @NonNull VideoSelectionOptions options, @NonNull Messages.Result> result) { if (!setPendingOptionsAndResult(null, options, result)) { finishWithAlreadyActiveError(result); return; @@ -376,7 +373,7 @@ private void launchTakeVideoWithCameraIntent() { public void chooseImageFromGallery( @NonNull ImageSelectionOptions options, boolean usePhotoPicker, - Messages.Result> result) { + @NonNull Messages.Result> result) { if (!setPendingOptionsAndResult(options, null, result)) { finishWithAlreadyActiveError(result); return; @@ -388,7 +385,7 @@ public void chooseImageFromGallery( public void chooseMultiImageFromGallery( @NonNull ImageSelectionOptions options, boolean usePhotoPicker, - Messages.Result> result) { + @NonNull Messages.Result> result) { if (!setPendingOptionsAndResult(options, null, result)) { finishWithAlreadyActiveError(result); return; @@ -436,7 +433,7 @@ private void launchMultiPickImageFromGalleryIntent(Boolean useAndroidPhotoPicker } public void takeImageWithCamera( - @NonNull ImageSelectionOptions options, Messages.Result> result) { + @NonNull ImageSelectionOptions options, @NonNull Messages.Result> result) { if (!setPendingOptionsAndResult(options, null, result)) { finishWithAlreadyActiveError(result); return; @@ -556,7 +553,8 @@ public boolean onRequestPermissionsResult( } @Override - public boolean onActivityResult(final int requestCode, final int resultCode, final Intent data) { + public boolean onActivityResult( + final int requestCode, final int resultCode, final @Nullable Intent data) { Runnable handlerRunnable; switch (requestCode) { @@ -605,7 +603,7 @@ private void handleChooseMultiImageResult(int resultCode, Intent intent) { } else { paths.add(fileUtils.getPathFromUri(activity, intent.getData())); } - handleMultiImageResult(paths, false); + handleMultiImageResult(paths); return; } @@ -632,12 +630,7 @@ private void handleCaptureImageResult(int resultCode) { localPendingCameraMediaUri != null ? localPendingCameraMediaUri : Uri.parse(cache.retrievePendingCameraMediaUriPath()), - new OnPathReadyListener() { - @Override - public void onPathReady(String path) { - handleImageResult(path, true); - } - }); + path -> handleImageResult(path, true)); return; } @@ -652,12 +645,7 @@ private void handleCaptureVideoResult(int resultCode) { localPendingCameraMediaUrl != null ? localPendingCameraMediaUrl : Uri.parse(cache.retrievePendingCameraMediaUriPath()), - new OnPathReadyListener() { - @Override - public void onPathReady(String path) { - handleVideoResult(path); - } - }); + this::handleVideoResult); return; } @@ -665,8 +653,7 @@ public void onPathReady(String path) { finishWithSuccess(null); } - private void handleMultiImageResult( - ArrayList paths, boolean shouldDeleteOriginalIfScaled) { + private void handleMultiImageResult(ArrayList paths) { ImageSelectionOptions localImageOptions = null; synchronized (pendingCallStateLock) { if (pendingCallState != null) { @@ -678,13 +665,6 @@ private void handleMultiImageResult( ArrayList finalPath = new ArrayList<>(); for (int i = 0; i < paths.size(); i++) { String finalImagePath = getResizedImagePath(paths.get(i), localImageOptions); - - //delete original file if scaled - if (finalImagePath != null - && !finalImagePath.equals(paths.get(i)) - && shouldDeleteOriginalIfScaled) { - new File(paths.get(i)).delete(); - } finalPath.add(i, finalImagePath); } finishWithListSuccess(finalPath); @@ -693,7 +673,7 @@ private void handleMultiImageResult( } } - private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) { + void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) { ImageSelectionOptions localImageOptions = null; synchronized (pendingCallStateLock) { if (pendingCallState != null) { @@ -721,7 +701,7 @@ private String getResizedImagePath(String path, @NonNull ImageSelectionOptions o outputOptions.getQuality().intValue()); } - private void handleVideoResult(String path) { + void handleVideoResult(String path) { finishWithSuccess(path); } diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java index cbf52bd07782..330759208381 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java @@ -176,20 +176,18 @@ ImagePickerDelegate getDelegate() { } private FlutterPluginBinding pluginBinding; - private ActivityState activityState; + ActivityState activityState; @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) { if (registrar.activity() == null) { // If a background flutter view tries to register the plugin, there will be no activity from the registrar, // we stop the registering process immediately because the ImagePicker requires an activity. return; } Activity activity = registrar.activity(); - Application application = null; - if (registrar.context() != null) { - application = (Application) (registrar.context().getApplicationContext()); - } + Application application = (Application) (registrar.context().getApplicationContext()); ImagePickerPlugin plugin = new ImagePickerPlugin(); plugin.setup(registrar.messenger(), application, activity, registrar, null); } @@ -305,7 +303,7 @@ public void pickImages( @NonNull Messages.ImageSelectionOptions options, @NonNull Boolean allowMultiple, @NonNull Boolean usePhotoPicker, - Result> result) { + @NonNull Result> result) { ImagePickerDelegate delegate = getImagePickerDelegate(); if (delegate == null) { result.error( @@ -335,7 +333,7 @@ public void pickVideos( @NonNull Messages.VideoSelectionOptions options, @NonNull Boolean allowMultiple, @NonNull Boolean usePhotoPicker, - Result> result) { + @NonNull Result> result) { ImagePickerDelegate delegate = getImagePickerDelegate(); if (delegate == null) { result.error( diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java index 5d0d5d8938d8..9ec4c0aa3baa 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java @@ -7,7 +7,9 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.SizeFCompat; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -30,8 +32,8 @@ class ImageResizer { */ String resizeImageIfNeeded( String imagePath, @Nullable Double maxWidth, @Nullable Double maxHeight, int imageQuality) { - Bitmap bmp = decodeFile(imagePath); - if (bmp == null) { + SizeFCompat originalSize = readFileDimensions(imagePath); + if (originalSize.getWidth() == -1 || originalSize.getHeight() == -1) { return imagePath; } boolean shouldScale = maxWidth != null || maxHeight != null || imageQuality < 100; @@ -41,7 +43,26 @@ String resizeImageIfNeeded( try { String[] pathParts = imagePath.split("/"); String imageName = pathParts[pathParts.length - 1]; - File file = resizedImage(bmp, maxWidth, maxHeight, imageQuality, imageName); + SizeFCompat targetSize = + calculateTargetSize( + (double) originalSize.getWidth(), + (double) originalSize.getHeight(), + maxWidth, + maxHeight); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = + calculateSampleSize(options, (int) targetSize.getWidth(), (int) targetSize.getHeight()); + Bitmap bmp = decodeFile(imagePath, options); + if (bmp == null) { + return imagePath; + } + File file = + resizedImage( + bmp, + (double) targetSize.getWidth(), + (double) targetSize.getHeight(), + imageQuality, + imageName); copyExif(imagePath, file.getPath()); return file.getPath(); } catch (IOException e) { @@ -50,10 +71,19 @@ String resizeImageIfNeeded( } private File resizedImage( - Bitmap bmp, Double maxWidth, Double maxHeight, int imageQuality, String outputImageName) + Bitmap bmp, Double width, Double height, int imageQuality, String outputImageName) throws IOException { - double originalWidth = bmp.getWidth() * 1.0; - double originalHeight = bmp.getHeight() * 1.0; + Bitmap scaledBmp = createScaledBitmap(bmp, width.intValue(), height.intValue(), false); + File file = + createImageOnExternalDirectory("/scaled_" + outputImageName, scaledBmp, imageQuality); + return file; + } + + private SizeFCompat calculateTargetSize( + @NonNull Double originalWidth, + @NonNull Double originalHeight, + @Nullable Double maxWidth, + @Nullable Double maxHeight) { boolean hasMaxWidth = maxWidth != null; boolean hasMaxHeight = maxHeight != null; @@ -90,10 +120,7 @@ private File resizedImage( } } - Bitmap scaledBmp = createScaledBitmap(bmp, width.intValue(), height.intValue(), false); - File file = - createImageOnExternalDirectory("/scaled_" + outputImageName, scaledBmp, imageQuality); - return file; + return new SizeFCompat(width.floatValue(), height.floatValue()); } private File createFile(File externalFilesDirectory, String child) { @@ -112,14 +139,47 @@ private void copyExif(String filePathOri, String filePathDest) { exifDataCopier.copyExif(filePathOri, filePathDest); } - private Bitmap decodeFile(String path) { - return BitmapFactory.decodeFile(path); + private SizeFCompat readFileDimensions(String path) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + decodeFile(path, options); + return new SizeFCompat(options.outWidth, options.outHeight); + } + + private Bitmap decodeFile(String path, @Nullable BitmapFactory.Options opts) { + return BitmapFactory.decodeFile(path, opts); } private Bitmap createScaledBitmap(Bitmap bmp, int width, int height, boolean filter) { return Bitmap.createScaledBitmap(bmp, width, height, filter); } + /** + * Calculates the largest sample size value that is a power of two based on a target width and + * height. + * + *

This value is necessary to tell the Bitmap decoder to subsample the original image, + * returning a smaller image to save memory. + * + * @see + * Loading Large Bitmaps Efficiently + */ + private int calculateSampleSize( + BitmapFactory.Options options, int targetWidth, int targetHeight) { + final int height = options.outHeight; + final int width = options.outWidth; + int sampleSize = 1; + if (height > targetHeight || width > targetWidth) { + final int halfHeight = height / 2; + final int halfWidth = width / 2; + while ((halfHeight / sampleSize) >= targetHeight && (halfWidth / sampleSize) >= targetWidth) { + sampleSize *= 2; + } + } + return sampleSize; + } + private File createImageOnExternalDirectory(String name, Bitmap bitmap, int imageQuality) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java index af98078c673c..accf64b7e734 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.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.0), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.imagepicker; @@ -39,7 +39,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; @@ -59,7 +59,7 @@ public enum SourceCamera { REAR(0), FRONT(1); - private final int index; + final int index; private SourceCamera(final int index) { this.index = index; @@ -70,7 +70,7 @@ public enum SourceType { CAMERA(0), GALLERY(1); - private final int index; + final int index; private SourceType(final int index) { this.index = index; @@ -81,7 +81,7 @@ public enum CacheRetrievalType { IMAGE(0), VIDEO(1); - private final int index; + final int index; private CacheRetrievalType(final int index) { this.index = index; @@ -134,8 +134,8 @@ public void setQuality(@NonNull Long setterArg) { this.quality = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private ImageSelectionOptions() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + ImageSelectionOptions() {} public static final class Builder { @@ -275,8 +275,8 @@ public void setCamera(@Nullable SourceCamera setterArg) { this.camera = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private SourceSpecification() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + SourceSpecification() {} public static final class Builder { @@ -351,8 +351,8 @@ public void setMessage(@Nullable String setterArg) { this.message = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private CacheRetrievalError() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + CacheRetrievalError() {} public static final class Builder { @@ -446,8 +446,8 @@ public void setPaths(@NonNull List setterArg) { this.paths = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private CacheRetrievalResult() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + CacheRetrievalResult() {} public static final class Builder { @@ -504,9 +504,10 @@ ArrayList toList() { } public interface Result { + @SuppressWarnings("UnknownNullness") void success(T result); - void error(Throwable error); + void error(@NonNull Throwable error); } private static class ImagePickerApiCodec extends StandardMessageCodec { @@ -568,7 +569,7 @@ void pickImages( @NonNull ImageSelectionOptions options, @NonNull Boolean allowMultiple, @NonNull Boolean usePhotoPicker, - Result> result); + @NonNull Result> result); /** * Selects video and returns their paths. * @@ -580,17 +581,17 @@ void pickVideos( @NonNull VideoSelectionOptions options, @NonNull Boolean allowMultiple, @NonNull Boolean usePhotoPicker, - Result> result); + @NonNull Result> result); /** Returns results from a previous app session, if any. */ @Nullable CacheRetrievalResult retrieveLostResults(); /** The codec used by ImagePickerApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return ImagePickerApiCodec.INSTANCE; } /** Sets up an instance of `ImagePickerApi` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, ImagePickerApi api) { + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ImagePickerApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java index d125b78150cc..06315f99f990 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java @@ -5,10 +5,16 @@ package io.flutter.plugins.imagepicker; import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.content.ContentProvider; +import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -66,7 +72,22 @@ public void FileUtil_GetPathFromUri() throws IOException { assertTrue(bytes.length > 0); String imageStream = new String(bytes, UTF_8); - assertTrue(imageStream.equals("imageStream")); + assertEquals("imageStream", imageStream); + } + + @Test + public void FileUtil_GetPathFromUri_securityException() throws IOException { + Uri uri = Uri.parse("content://dummy/dummy.png"); + + ContentResolver mockContentResolver = mock(ContentResolver.class); + when(mockContentResolver.openInputStream(any(Uri.class))).thenThrow(SecurityException.class); + + Context mockContext = mock(Context.class); + when(mockContext.getContentResolver()).thenReturn(mockContentResolver); + + String path = fileUtils.getPathFromUri(mockContext, uri); + + assertNull(path); } @Test diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java index 45b2a45e8eeb..a2f742f23349 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java @@ -6,17 +6,24 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import java.io.File; import java.io.IOException; +import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -102,4 +109,32 @@ public void onResizeImageIfNeeded_whenImagePathIsNotBitmap_shouldReturnPathAndNo assertThat(resizedImagePath, equalTo(nonBitmapImagePath)); } } + + @Test + public void onResizeImageIfNeeded_whenResizeIsNotNecessary_shouldOnlyQueryBitmapDimensions() { + try (MockedStatic mockBitmapFactory = + mockStatic(BitmapFactory.class, Mockito.CALLS_REAL_METHODS)) { + String outputFile = resizer.resizeImageIfNeeded(imageFile.getPath(), null, null, 100); + ArgumentCaptor argument = + ArgumentCaptor.forClass(BitmapFactory.Options.class); + mockBitmapFactory.verify(() -> BitmapFactory.decodeFile(anyString(), argument.capture())); + BitmapFactory.Options capturedOptions = argument.getValue(); + assertTrue(capturedOptions.inJustDecodeBounds); + } + } + + @Test + public void onResizeImageIfNeeded_whenResizeIsNecessary_shouldDecodeBitmapPixels() { + try (MockedStatic mockBitmapFactory = + mockStatic(BitmapFactory.class, Mockito.CALLS_REAL_METHODS)) { + String outputFile = resizer.resizeImageIfNeeded(imageFile.getPath(), 50.0, 50.0, 100); + ArgumentCaptor argument = + ArgumentCaptor.forClass(BitmapFactory.Options.class); + mockBitmapFactory.verify( + () -> BitmapFactory.decodeFile(anyString(), argument.capture()), times(2)); + List capturedOptions = argument.getAllValues(); + assertTrue(capturedOptions.get(0).inJustDecodeBounds); + assertFalse(capturedOptions.get(1).inJustDecodeBounds); + } + } } diff --git a/packages/image_picker/image_picker_android/example/.pluginToolsConfig.yaml b/packages/image_picker/image_picker_android/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/image_picker/image_picker_android/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/image_picker/image_picker_android/example/lib/main.dart b/packages/image_picker/image_picker_android/example/lib/main.dart index c456fb98dc1c..fa87587857b0 100755 --- a/packages/image_picker/image_picker_android/example/lib/main.dart +++ b/packages/image_picker/image_picker_android/example/lib/main.dart @@ -98,55 +98,57 @@ class _MyHomePageState extends State { } Future _onImageButtonPressed( - BuildContext context, { - required ImageSource source, + ImageSource source, { + required BuildContext context, bool isMultiImage = false, }) async { if (_controller != null) { await _controller!.setVolume(0.0); } - if (isVideo) { - final XFile? file = await _picker.getVideo( - source: source, maxDuration: const Duration(seconds: 10)); - if (file != null && context.mounted) { - _showPickedSnackBar(context, [file]); - } - await _playVideo(file); - } else if (isMultiImage && context.mounted) { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final List? pickedFileList = await _picker.getMultiImage( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); - if (pickedFileList != null && context.mounted) { - _showPickedSnackBar(context, pickedFileList); - } - setState(() => _imageFileList = pickedFileList); - } catch (e) { - setState(() => _pickImageError = e); + if (context.mounted) { + if (isVideo) { + final XFile? file = await _picker.getVideo( + source: source, maxDuration: const Duration(seconds: 10)); + if (file != null && context.mounted) { + _showPickedSnackBar(context, [file]); } - }); - } else { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final XFile? pickedFile = await _picker.getImage( - source: source, - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); - if (pickedFile != null && context.mounted) { - _showPickedSnackBar(context, [pickedFile]); + await _playVideo(file); + } else if (isMultiImage) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List? pickedFileList = await _picker.getMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + if (pickedFileList != null && context.mounted) { + _showPickedSnackBar(context, pickedFileList); + } + setState(() => _imageFileList = pickedFileList); + } catch (e) { + setState(() => _pickImageError = e); } - setState(() => _setImageFileListFromFile(pickedFile)); - } catch (e) { - setState(() => _pickImageError = e); - } - }); + }); + } else { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + if (pickedFile != null && context.mounted) { + _showPickedSnackBar(context, [pickedFile]); + } + setState(() => _setImageFileListFromFile(pickedFile)); + } catch (e) { + setState(() => _pickImageError = e); + } + }); + } } } @@ -315,7 +317,7 @@ class _MyHomePageState extends State { key: const Key('image_picker_example_from_gallery'), onPressed: () { isVideo = false; - _onImageButtonPressed(context, source: ImageSource.gallery); + _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'image0', tooltip: 'Pick Image from gallery', @@ -328,8 +330,8 @@ class _MyHomePageState extends State { onPressed: () { isVideo = false; _onImageButtonPressed( - context, - source: ImageSource.gallery, + ImageSource.gallery, + context: context, isMultiImage: true, ); }, @@ -343,7 +345,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( onPressed: () { isVideo = false; - _onImageButtonPressed(context, source: ImageSource.camera); + _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'image2', tooltip: 'Take a Photo', @@ -356,7 +358,7 @@ class _MyHomePageState extends State { backgroundColor: Colors.red, onPressed: () { isVideo = true; - _onImageButtonPressed(context, source: ImageSource.gallery); + _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'video0', tooltip: 'Pick Video from gallery', @@ -369,7 +371,7 @@ class _MyHomePageState extends State { backgroundColor: Colors.red, onPressed: () { isVideo = true; - _onImageButtonPressed(context, source: ImageSource.camera); + _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'video1', tooltip: 'Take a Video', diff --git a/packages/image_picker/image_picker_android/lib/src/messages.g.dart b/packages/image_picker/image_picker_android/lib/src/messages.g.dart index c0506ad4daec..a4f15c847559 100644 --- a/packages/image_picker/image_picker_android/lib/src/messages.g.dart +++ b/packages/image_picker/image_picker_android/lib/src/messages.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.0), 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/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml index ec6a0b3b7678..0ee39768d7c8 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+10 +version: 0.8.6+16 environment: sdk: ">=2.18.0 <4.0.0" @@ -28,4 +28,4 @@ dev_dependencies: flutter_test: sdk: flutter mockito: 5.4.0 - pigeon: ^9.1.0 + pigeon: ^9.2.5 diff --git a/packages/image_picker/image_picker_android/test/test_api.g.dart b/packages/image_picker/image_picker_android/test/test_api.g.dart index aa5c38f52eb1..dbb6b143a91d 100644 --- a/packages/image_picker/image_picker_android/test/test_api.g.dart +++ b/packages/image_picker/image_picker_android/test/test_api.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.0), 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 @@ -57,6 +57,8 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { } abstract class TestHostImagePickerApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = _TestHostImagePickerApiCodec(); /// Selects images and returns their paths. @@ -83,9 +85,12 @@ abstract class TestHostImagePickerApi { 'dev.flutter.pigeon.ImagePickerApi.pickImages', 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.ImagePickerApi.pickImages was null.'); final List args = (message as List?)!; @@ -114,9 +119,12 @@ abstract class TestHostImagePickerApi { 'dev.flutter.pigeon.ImagePickerApi.pickVideos', 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.ImagePickerApi.pickVideos was null.'); final List args = (message as List?)!; @@ -145,9 +153,12 @@ abstract class TestHostImagePickerApi { 'dev.flutter.pigeon.ImagePickerApi.retrieveLostResults', 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 CacheRetrievalResult? output = api.retrieveLostResults(); return [output]; diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index f8bc98e5245f..100a9b0490f8 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.1.12 * Clarifies explanation of endorsement in README. diff --git a/packages/image_picker/image_picker_for_web/example/pubspec.yaml b/packages/image_picker/image_picker_for_web/example/pubspec.yaml index c150c20e6c47..9c431bd6e90d 100644 --- a/packages/image_picker/image_picker_for_web/example/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/example/pubspec.yaml @@ -2,8 +2,8 @@ name: image_picker_for_web_integration_tests publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index 3fe4071d25a1..06a7093f5962 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.1.12 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md index c03b6462a9c0..1173ddf27b7b 100644 --- a/packages/image_picker/image_picker_ios/CHANGELOG.md +++ b/packages/image_picker/image_picker_ios/CHANGELOG.md @@ -1,4 +1,5 @@ -## NEXT +## 0.8.7+4 +* Fixes `BuildContext` handling in example. * Updates metadata unit test to work on iOS 16.2. ## 0.8.7+3 diff --git a/packages/image_picker/image_picker_ios/example/lib/main.dart b/packages/image_picker/image_picker_ios/example/lib/main.dart index 7f8956a9f2c5..76076a5dbd65 100755 --- a/packages/image_picker/image_picker_ios/example/lib/main.dart +++ b/packages/image_picker/image_picker_ios/example/lib/main.dart @@ -81,58 +81,60 @@ class _MyHomePageState extends State { } Future _onImageButtonPressed(ImageSource source, - {BuildContext? context, bool isMultiImage = false}) async { + {required BuildContext context, bool isMultiImage = false}) async { if (_controller != null) { await _controller!.setVolume(0.0); } - if (isVideo) { - final XFile? file = await _picker.getVideo( - source: source, maxDuration: const Duration(seconds: 10)); - await _playVideo(file); - } else if (isMultiImage) { - await _displayPickImageDialog(context!, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final List pickedFileList = - await _picker.getMultiImageWithOptions( - options: MultiImagePickerOptions( - imageOptions: ImageOptions( + if (context.mounted) { + if (isVideo) { + final XFile? file = await _picker.getVideo( + source: source, maxDuration: const Duration(seconds: 10)); + await _playVideo(file); + } else if (isMultiImage) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = + await _picker.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), + ); + setState(() { + _imageFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImageFromSource( + source: source, + options: ImagePickerOptions( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: quality, ), - ), - ); - setState(() { - _imageFileList = pickedFileList; - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } else { - await _displayPickImageDialog(context!, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final XFile? pickedFile = await _picker.getImageFromSource( - source: source, - options: ImagePickerOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), - ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } } } @@ -281,7 +283,7 @@ class _MyHomePageState extends State { backgroundColor: Colors.red, onPressed: () { isVideo = true; - _onImageButtonPressed(ImageSource.gallery); + _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'video0', tooltip: 'Pick Video from gallery', @@ -294,7 +296,7 @@ class _MyHomePageState extends State { backgroundColor: Colors.red, onPressed: () { isVideo = true; - _onImageButtonPressed(ImageSource.camera); + _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'video1', tooltip: 'Take a Video', diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml index 8665e93aca21..99c94fab962a 100755 --- a/packages/image_picker/image_picker_ios/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_ios description: iOS implementation of the image_picker plugin. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_ios 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_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 2977e2921ba7..1308001548b8 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. * Aligns Dart and Flutter SDK constraints. ## 2.6.3 diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index e03c59b8d4b1..5bc22aecd9ee 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -7,8 +7,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.6.3 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: cross_file: ^0.3.1+1 diff --git a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md index ce33145e2ef3..c049d007a913 100644 --- a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. * Aligns Dart and Flutter SDK constraints. ## 3.1.5 diff --git a/packages/in_app_purchase/in_app_purchase/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/in_app_purchase/in_app_purchase/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/in_app_purchase/in_app_purchase/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/in_app_purchase/in_app_purchase/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/in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/project.pbxproj index 113455d6e179..924f81e05833 100644 --- a/packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -255,6 +255,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml index 5ec9ddf7d916..e3c891e822ec 100644 --- a/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the in_app_purchase plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml index 4b0a2e519ca4..d873338e28d9 100644 --- a/packages/in_app_purchase/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 3.1.5 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: 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 cc8101dc4f27..cf62ad38ec96 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,15 @@ +## 0.3.0+2 + +* Removes obsolete null checks on non-nullable values. + +## 0.3.0+1 + +* Fixes misaligned method signature strings. + +## 0.3.0 +* **BREAKING CHANGE**: Removes `launchPriceChangeConfirmationFlow` from `InAppPurchaseAndroidPlatform`. Price changes are now [handled by Google Play](https://developer.android.com/google/play/billing/subscriptions#price-change). +* Returns both base plans and offers when `queryProductDetailsAsync` is called. + ## 0.2.5+5 * Updates gradle, AGP and fixes some lint errors. diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java index 6f4e4bbfd8ee..c02bc8e893b3 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java @@ -32,23 +32,21 @@ static final class MethodNames { "BillingClient#startConnection(BillingClientStateListener)"; static final String END_CONNECTION = "BillingClient#endConnection()"; static final String ON_DISCONNECT = "BillingClientStateListener#onBillingServiceDisconnected()"; - static final String QUERY_SKU_DETAILS = - "BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)"; + static final String QUERY_PRODUCT_DETAILS = + "BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)"; static final String LAUNCH_BILLING_FLOW = "BillingClient#launchBillingFlow(Activity, BillingFlowParams)"; static final String ON_PURCHASES_UPDATED = - "PurchasesUpdatedListener#onPurchasesUpdated(int, List)"; - static final String QUERY_PURCHASES = "BillingClient#queryPurchases(String)"; - static final String QUERY_PURCHASES_ASYNC = "BillingClient#queryPurchasesAsync(String)"; + "PurchasesUpdatedListener#onPurchasesUpdated(BillingResult, List)"; + static final String QUERY_PURCHASES_ASYNC = + "BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)"; static final String QUERY_PURCHASE_HISTORY_ASYNC = - "BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)"; + "BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)"; static final String CONSUME_PURCHASE_ASYNC = - "BillingClient#consumeAsync(String, ConsumeResponseListener)"; + "BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)"; static final String ACKNOWLEDGE_PURCHASE = - "BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)"; + "BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)"; static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)"; - static final String LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW = - "BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)"; static final String GET_CONNECTION_STATE = "BillingClient#getConnectionState()"; private MethodNames() {}; diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index b6a27561d9d5..c2ce590eecd2 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -4,9 +4,11 @@ package io.flutter.plugins.inapppurchase; +import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; +import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; -import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList; +import static io.flutter.plugins.inapppurchase.Translator.toProductList; import android.app.Activity; import android.app.Application; @@ -24,14 +26,19 @@ import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.ConsumeResponseListener; +import com.android.billingclient.api.ProductDetails; +import com.android.billingclient.api.ProductDetailsResponseListener; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.PurchaseHistoryRecord; import com.android.billingclient.api.PurchaseHistoryResponseListener; import com.android.billingclient.api.PurchasesResponseListener; +import com.android.billingclient.api.QueryProductDetailsParams; +import com.android.billingclient.api.QueryProductDetailsParams.Product; import com.android.billingclient.api.QueryPurchaseHistoryParams; import com.android.billingclient.api.QueryPurchasesParams; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -41,7 +48,7 @@ class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler, Application.ActivityLifecycleCallbacks { private static final String TAG = "InAppPurchasePlugin"; - private static final String LOAD_SKU_DOC_URL = + private static final String LOAD_PRODUCT_DOC_URL = "https://github.com/flutter/packages/blob/main/packages/in_app_purchase/in_app_purchase/README.md#loading-products-for-sale"; @Nullable private BillingClient billingClient; @@ -51,9 +58,7 @@ class MethodCallHandlerImpl private final Context applicationContext; private final MethodChannel methodChannel; - // TODO(stuartmorgan): Migrate this code. See TODO on querySkuDetailsAsync. - @SuppressWarnings("deprecation") - private HashMap cachedSkus = new HashMap<>(); + private final HashMap cachedProducts = new HashMap<>(); /** Constructs the MethodCallHandlerImpl */ MethodCallHandlerImpl( @@ -117,31 +122,28 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { case InAppPurchasePlugin.MethodNames.END_CONNECTION: endConnection(result); break; - case InAppPurchasePlugin.MethodNames.QUERY_SKU_DETAILS: - List skusList = call.argument("skusList"); - querySkuDetailsAsync((String) call.argument("skuType"), skusList, result); + case InAppPurchasePlugin.MethodNames.QUERY_PRODUCT_DETAILS: + List productList = toProductList(call.argument("productList")); + queryProductDetailsAsync(productList, result); break; case InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW: launchBillingFlow( - (String) call.argument("sku"), + (String) call.argument("product"), + (String) call.argument("offerToken"), (String) call.argument("accountId"), (String) call.argument("obfuscatedProfileId"), - (String) call.argument("oldSku"), + (String) call.argument("oldProduct"), (String) call.argument("purchaseToken"), call.hasArgument("prorationMode") ? (int) call.argument("prorationMode") : ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY, result); break; - case InAppPurchasePlugin.MethodNames.QUERY_PURCHASES: // Legacy method name. - queryPurchasesAsync((String) call.argument("skuType"), result); - break; case InAppPurchasePlugin.MethodNames.QUERY_PURCHASES_ASYNC: - queryPurchasesAsync((String) call.argument("skuType"), result); + queryPurchasesAsync((String) call.argument("productType"), result); break; case InAppPurchasePlugin.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC: - Log.e("flutter", (String) call.argument("skuType")); - queryPurchaseHistoryAsync((String) call.argument("skuType"), result); + queryPurchaseHistoryAsync((String) call.argument("productType"), result); break; case InAppPurchasePlugin.MethodNames.CONSUME_PURCHASE_ASYNC: consumeAsync((String) call.argument("purchaseToken"), result); @@ -152,9 +154,6 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { case InAppPurchasePlugin.MethodNames.IS_FEATURE_SUPPORTED: isFeatureSupported((String) call.argument("feature"), result); break; - case InAppPurchasePlugin.MethodNames.LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW: - launchPriceChangeConfirmationFlow((String) call.argument("sku"), result); - break; case InAppPurchasePlugin.MethodNames.GET_CONNECTION_STATE: getConnectionState(result); break; @@ -183,77 +182,95 @@ private void isReady(MethodChannel.Result result) { result.success(billingClient.isReady()); } - // TODO(stuartmorgan): Migrate to new subscriptions API. See: - // - https://developer.android.com/google/play/billing/migrate-gpblv5 - // - https://github.com/flutter/flutter/issues/114265 - // - https://github.com/flutter/flutter/issues/107370 - @SuppressWarnings("deprecation") - private void querySkuDetailsAsync( - final String skuType, final List skusList, final MethodChannel.Result result) { + private void queryProductDetailsAsync( + final List productList, final MethodChannel.Result result) { if (billingClientError(result)) { return; } - com.android.billingclient.api.SkuDetailsParams params = - com.android.billingclient.api.SkuDetailsParams.newBuilder() - .setType(skuType) - .setSkusList(skusList) - .build(); - billingClient.querySkuDetailsAsync( + QueryProductDetailsParams params = + QueryProductDetailsParams.newBuilder().setProductList(productList).build(); + billingClient.queryProductDetailsAsync( params, - new com.android.billingclient.api.SkuDetailsResponseListener() { + new ProductDetailsResponseListener() { @Override - public void onSkuDetailsResponse( - BillingResult billingResult, - List skuDetailsList) { - updateCachedSkus(skuDetailsList); - final Map skuDetailsResponse = new HashMap<>(); - skuDetailsResponse.put("billingResult", Translator.fromBillingResult(billingResult)); - skuDetailsResponse.put("skuDetailsList", fromSkuDetailsList(skuDetailsList)); - result.success(skuDetailsResponse); + public void onProductDetailsResponse( + @NonNull BillingResult billingResult, + @NonNull List productDetailsList) { + updateCachedProducts(productDetailsList); + final Map productDetailsResponse = new HashMap<>(); + productDetailsResponse.put("billingResult", fromBillingResult(billingResult)); + productDetailsResponse.put( + "productDetailsList", fromProductDetailsList(productDetailsList)); + result.success(productDetailsResponse); } }); } private void launchBillingFlow( - String sku, + String product, + @Nullable String offerToken, @Nullable String accountId, @Nullable String obfuscatedProfileId, - @Nullable String oldSku, + @Nullable String oldProduct, @Nullable String purchaseToken, int prorationMode, MethodChannel.Result result) { if (billingClientError(result)) { return; } - // TODO(stuartmorgan): Migrate this code. See TODO on querySkuDetailsAsync. - @SuppressWarnings("deprecation") - com.android.billingclient.api.SkuDetails skuDetails = cachedSkus.get(sku); - if (skuDetails == null) { + + com.android.billingclient.api.ProductDetails productDetails = cachedProducts.get(product); + if (productDetails == null) { result.error( "NOT_FOUND", - "Details for sku " - + sku - + " are not available. It might because skus were not fetched prior to the call. Please fetch the skus first. An example of how to fetch the skus could be found here: " - + LOAD_SKU_DOC_URL, + "Details for product " + + product + + " are not available. It might because products were not fetched prior to the call. Please fetch the products first. An example of how to fetch the products could be found here: " + + LOAD_PRODUCT_DOC_URL, null); return; } - if (oldSku == null + @Nullable + List subscriptionOfferDetails = + productDetails.getSubscriptionOfferDetails(); + if (subscriptionOfferDetails != null) { + boolean isValidOfferToken = false; + for (ProductDetails.SubscriptionOfferDetails offerDetails : subscriptionOfferDetails) { + if (offerToken != null && offerToken.equals(offerDetails.getOfferToken())) { + isValidOfferToken = true; + break; + } + } + if (!isValidOfferToken) { + result.error( + "INVALID_OFFER_TOKEN", + "Offer token " + + offerToken + + " for product " + + product + + " is not valid. Make sure to only pass offer tokens that belong to the product. To obtain offer tokens for a product, fetch the products. An example of how to fetch the products could be found here: " + + LOAD_PRODUCT_DOC_URL, + null); + return; + } + } + + if (oldProduct == null && prorationMode != ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) { result.error( - "IN_APP_PURCHASE_REQUIRE_OLD_SKU", - "launchBillingFlow failed because oldSku is null. You must provide a valid oldSku in order to use a proration mode.", + "IN_APP_PURCHASE_REQUIRE_OLD_PRODUCT", + "launchBillingFlow failed because oldProduct is null. You must provide a valid oldProduct in order to use a proration mode.", null); return; - } else if (oldSku != null && !cachedSkus.containsKey(oldSku)) { + } else if (oldProduct != null && !cachedProducts.containsKey(oldProduct)) { result.error( - "IN_APP_PURCHASE_INVALID_OLD_SKU", - "Details for sku " - + oldSku - + " are not available. It might because skus were not fetched prior to the call. Please fetch the skus first. An example of how to fetch the skus could be found here: " - + LOAD_SKU_DOC_URL, + "IN_APP_PURCHASE_INVALID_OLD_PRODUCT", + "Details for product " + + oldProduct + + " are not available. It might because products were not fetched prior to the call. Please fetch the products first. An example of how to fetch the products could be found here: " + + LOAD_PRODUCT_DOC_URL, null); return; } @@ -261,17 +278,25 @@ private void launchBillingFlow( if (activity == null) { result.error( "ACTIVITY_UNAVAILABLE", - "Details for sku " - + sku + "Details for product " + + product + " are not available. This method must be run with the app in foreground.", null); return; } - // TODO(stuartmorgan): Migrate this code. See TODO on querySkuDetailsAsync. - @SuppressWarnings("deprecation") + BillingFlowParams.ProductDetailsParams.Builder productDetailsParamsBuilder = + BillingFlowParams.ProductDetailsParams.newBuilder(); + productDetailsParamsBuilder.setProductDetails(productDetails); + if (offerToken != null) { + productDetailsParamsBuilder.setOfferToken(offerToken); + } + + List productDetailsParamsList = new ArrayList<>(); + productDetailsParamsList.add(productDetailsParamsBuilder.build()); + BillingFlowParams.Builder paramsBuilder = - BillingFlowParams.newBuilder().setSkuDetails(skuDetails); + BillingFlowParams.newBuilder().setProductDetailsParamsList(productDetailsParamsList); if (accountId != null && !accountId.isEmpty()) { paramsBuilder.setObfuscatedAccountId(accountId); } @@ -280,7 +305,7 @@ private void launchBillingFlow( } BillingFlowParams.SubscriptionUpdateParams.Builder subscriptionUpdateParamsBuilder = BillingFlowParams.SubscriptionUpdateParams.newBuilder(); - if (oldSku != null && !oldSku.isEmpty() && purchaseToken != null) { + if (oldProduct != null && !oldProduct.isEmpty() && purchaseToken != null) { subscriptionUpdateParamsBuilder.setOldPurchaseToken(purchaseToken); // The proration mode value has to match one of the following declared in // https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode @@ -288,8 +313,7 @@ private void launchBillingFlow( paramsBuilder.setSubscriptionUpdateParams(subscriptionUpdateParamsBuilder.build()); } result.success( - Translator.fromBillingResult( - billingClient.launchBillingFlow(activity, paramsBuilder.build()))); + fromBillingResult(billingClient.launchBillingFlow(activity, paramsBuilder.build()))); } private void consumeAsync(String purchaseToken, final MethodChannel.Result result) { @@ -301,7 +325,7 @@ private void consumeAsync(String purchaseToken, final MethodChannel.Result resul new ConsumeResponseListener() { @Override public void onConsumeResponse(BillingResult billingResult, String outToken) { - result.success(Translator.fromBillingResult(billingResult)); + result.success(fromBillingResult(billingResult)); } }; ConsumeParams.Builder paramsBuilder = @@ -312,7 +336,7 @@ public void onConsumeResponse(BillingResult billingResult, String outToken) { billingClient.consumeAsync(params, listener); } - private void queryPurchasesAsync(String skuType, MethodChannel.Result result) { + private void queryPurchasesAsync(String productType, MethodChannel.Result result) { if (billingClientError(result)) { return; } @@ -320,7 +344,7 @@ private void queryPurchasesAsync(String skuType, MethodChannel.Result result) { // Like in our connect call, consider the billing client responding a "success" here regardless // of status code. QueryPurchasesParams.Builder paramsBuilder = QueryPurchasesParams.newBuilder(); - paramsBuilder.setProductType(skuType); + paramsBuilder.setProductType(productType); billingClient.queryPurchasesAsync( paramsBuilder.build(), new PurchasesResponseListener() { @@ -331,26 +355,26 @@ public void onQueryPurchasesResponse( // The response code is no longer passed, as part of billing 4.0, so we pass OK here // as success is implied by calling this callback. serialized.put("responseCode", BillingClient.BillingResponseCode.OK); - serialized.put("billingResult", Translator.fromBillingResult(billingResult)); + serialized.put("billingResult", fromBillingResult(billingResult)); serialized.put("purchasesList", fromPurchasesList(purchasesList)); result.success(serialized); } }); } - private void queryPurchaseHistoryAsync(String skuType, final MethodChannel.Result result) { + private void queryPurchaseHistoryAsync(String productType, final MethodChannel.Result result) { if (billingClientError(result)) { return; } billingClient.queryPurchaseHistoryAsync( - QueryPurchaseHistoryParams.newBuilder().setProductType(skuType).build(), + QueryPurchaseHistoryParams.newBuilder().setProductType(productType).build(), new PurchaseHistoryResponseListener() { @Override public void onPurchaseHistoryResponse( BillingResult billingResult, List purchasesList) { final Map serialized = new HashMap<>(); - serialized.put("billingResult", Translator.fromBillingResult(billingResult)); + serialized.put("billingResult", fromBillingResult(billingResult)); serialized.put( "purchaseHistoryRecordList", fromPurchaseHistoryRecordList(purchasesList)); result.success(serialized); @@ -385,7 +409,7 @@ public void onBillingSetupFinished(BillingResult billingResult) { alreadyFinished = true; // Consider the fact that we've finished a success, leave it to the Dart side to // validate the responseCode. - result.success(Translator.fromBillingResult(billingResult)); + result.success(fromBillingResult(billingResult)); } @Override @@ -408,67 +432,21 @@ private void acknowledgePurchase(String purchaseToken, final MethodChannel.Resul new AcknowledgePurchaseResponseListener() { @Override public void onAcknowledgePurchaseResponse(BillingResult billingResult) { - result.success(Translator.fromBillingResult(billingResult)); + result.success(fromBillingResult(billingResult)); } }); } - // TODO(stuartmorgan): Migrate this code. See TODO on querySkuDetailsAsync. - @SuppressWarnings("deprecation") - private void updateCachedSkus( - @Nullable List skuDetailsList) { - if (skuDetailsList == null) { + protected void updateCachedProducts(@Nullable List productDetailsList) { + if (productDetailsList == null) { return; } - for (com.android.billingclient.api.SkuDetails skuDetails : skuDetailsList) { - cachedSkus.put(skuDetails.getSku(), skuDetails); + for (ProductDetails productDetails : productDetailsList) { + cachedProducts.put(productDetails.getProductId(), productDetails); } } - // TODO(stuartmorgan): Migrate this code. See TODO on querySkuDetailsAsync. - @SuppressWarnings("deprecation") - private void launchPriceChangeConfirmationFlow(String sku, MethodChannel.Result result) { - if (activity == null) { - result.error( - "ACTIVITY_UNAVAILABLE", - "launchPriceChangeConfirmationFlow is not available. " - + "This method must be run with the app in foreground.", - null); - return; - } - if (billingClientError(result)) { - return; - } - // Note that assert doesn't work on Android (see https://stackoverflow.com/a/6176529/5167831 and https://stackoverflow.com/a/8164195/5167831) - // and that this assert is only added to silence the analyser. The actual null check - // is handled by the `billingClientError()` call. - assert billingClient != null; - - com.android.billingclient.api.SkuDetails skuDetails = cachedSkus.get(sku); - if (skuDetails == null) { - result.error( - "NOT_FOUND", - "Details for sku " - + sku - + " are not available. It might because skus were not fetched prior to the call. Please fetch the skus first. An example of how to fetch the skus could be found here: " - + LOAD_SKU_DOC_URL, - null); - return; - } - - com.android.billingclient.api.PriceChangeFlowParams params = - new com.android.billingclient.api.PriceChangeFlowParams.Builder() - .setSkuDetails(skuDetails) - .build(); - billingClient.launchPriceChangeConfirmationFlow( - activity, - params, - billingResult -> { - result.success(Translator.fromBillingResult(billingResult)); - }); - } - private boolean billingClientError(MethodChannel.Result result) { if (billingClient != null) { return false; diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java index 273a28474e92..9f397e4e9fb6 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java @@ -4,69 +4,173 @@ package io.flutter.plugins.inapppurchase; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.billingclient.api.AccountIdentifiers; import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.ProductDetails; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.PurchaseHistoryRecord; +import com.android.billingclient.api.QueryProductDetailsParams; import java.util.ArrayList; import java.util.Collections; import java.util.Currency; import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; -/** Handles serialization of {@link com.android.billingclient.api.BillingClient} related objects. */ +/** + * Handles serialization and deserialization of {@link com.android.billingclient.api.BillingClient} + * related objects. + */ /*package*/ class Translator { - // TODO(stuartmorgan): Migrate this code. See TODO on MethodCallHandlerImpl.querySkuDetailsAsync. - @SuppressWarnings("deprecation") - static HashMap fromSkuDetail(com.android.billingclient.api.SkuDetails detail) { + static HashMap fromProductDetail(ProductDetails detail) { HashMap info = new HashMap<>(); info.put("title", detail.getTitle()); info.put("description", detail.getDescription()); - info.put("freeTrialPeriod", detail.getFreeTrialPeriod()); - info.put("introductoryPrice", detail.getIntroductoryPrice()); - info.put("introductoryPriceAmountMicros", detail.getIntroductoryPriceAmountMicros()); - info.put("introductoryPriceCycles", detail.getIntroductoryPriceCycles()); - info.put("introductoryPricePeriod", detail.getIntroductoryPricePeriod()); - info.put("price", detail.getPrice()); - info.put("priceAmountMicros", detail.getPriceAmountMicros()); - info.put("priceCurrencyCode", detail.getPriceCurrencyCode()); - info.put("priceCurrencySymbol", currencySymbolFromCode(detail.getPriceCurrencyCode())); - info.put("sku", detail.getSku()); - info.put("type", detail.getType()); - info.put("subscriptionPeriod", detail.getSubscriptionPeriod()); - info.put("originalPrice", detail.getOriginalPrice()); - info.put("originalPriceAmountMicros", detail.getOriginalPriceAmountMicros()); + info.put("productId", detail.getProductId()); + info.put("productType", detail.getProductType()); + info.put("name", detail.getName()); + + @Nullable + ProductDetails.OneTimePurchaseOfferDetails oneTimePurchaseOfferDetails = + detail.getOneTimePurchaseOfferDetails(); + if (oneTimePurchaseOfferDetails != null) { + info.put( + "oneTimePurchaseOfferDetails", + fromOneTimePurchaseOfferDetails(oneTimePurchaseOfferDetails)); + } + + @Nullable + List subscriptionOfferDetailsList = + detail.getSubscriptionOfferDetails(); + if (subscriptionOfferDetailsList != null) { + info.put( + "subscriptionOfferDetails", + fromSubscriptionOfferDetailsList(subscriptionOfferDetailsList)); + } + return info; } - // TODO(stuartmorgan): Migrate this code. See TODO on MethodCallHandlerImpl.querySkuDetailsAsync. - @SuppressWarnings("deprecation") - static List> fromSkuDetailsList( - @Nullable List skuDetailsList) { - if (skuDetailsList == null) { + static List toProductList(List serialized) { + List products = new ArrayList<>(); + for (Object productSerialized : serialized) { + @SuppressWarnings(value = "unchecked") + Map productMap = (Map) productSerialized; + products.add(toProduct(productMap)); + } + return products; + } + + static QueryProductDetailsParams.Product toProduct(Map serialized) { + String productId = (String) serialized.get("productId"); + String productType = (String) serialized.get("productType"); + return QueryProductDetailsParams.Product.newBuilder() + .setProductId(productId) + .setProductType(productType) + .build(); + } + + static List> fromProductDetailsList( + @Nullable List productDetailsList) { + if (productDetailsList == null) { return Collections.emptyList(); } ArrayList> output = new ArrayList<>(); - for (com.android.billingclient.api.SkuDetails detail : skuDetailsList) { - output.add(fromSkuDetail(detail)); + for (ProductDetails detail : productDetailsList) { + output.add(fromProductDetail(detail)); } return output; } + static HashMap fromOneTimePurchaseOfferDetails( + @Nullable ProductDetails.OneTimePurchaseOfferDetails oneTimePurchaseOfferDetails) { + HashMap serialized = new HashMap<>(); + if (oneTimePurchaseOfferDetails == null) { + return serialized; + } + + serialized.put("priceAmountMicros", oneTimePurchaseOfferDetails.getPriceAmountMicros()); + serialized.put("priceCurrencyCode", oneTimePurchaseOfferDetails.getPriceCurrencyCode()); + serialized.put("formattedPrice", oneTimePurchaseOfferDetails.getFormattedPrice()); + + return serialized; + } + + static List> fromSubscriptionOfferDetailsList( + @Nullable List subscriptionOfferDetailsList) { + if (subscriptionOfferDetailsList == null) { + return Collections.emptyList(); + } + + ArrayList> serialized = new ArrayList<>(); + + for (ProductDetails.SubscriptionOfferDetails subscriptionOfferDetails : + subscriptionOfferDetailsList) { + serialized.add(fromSubscriptionOfferDetails(subscriptionOfferDetails)); + } + + return serialized; + } + + static HashMap fromSubscriptionOfferDetails( + @Nullable ProductDetails.SubscriptionOfferDetails subscriptionOfferDetails) { + HashMap serialized = new HashMap<>(); + if (subscriptionOfferDetails == null) { + return serialized; + } + + serialized.put("offerId", subscriptionOfferDetails.getOfferId()); + serialized.put("basePlanId", subscriptionOfferDetails.getBasePlanId()); + serialized.put("offerTags", subscriptionOfferDetails.getOfferTags()); + serialized.put("offerIdToken", subscriptionOfferDetails.getOfferToken()); + + ProductDetails.PricingPhases pricingPhases = subscriptionOfferDetails.getPricingPhases(); + serialized.put("pricingPhases", fromPricingPhases(pricingPhases)); + + return serialized; + } + + static List> fromPricingPhases( + @NonNull ProductDetails.PricingPhases pricingPhases) { + ArrayList> serialized = new ArrayList<>(); + + for (ProductDetails.PricingPhase pricingPhase : pricingPhases.getPricingPhaseList()) { + serialized.add(fromPricingPhase(pricingPhase)); + } + return serialized; + } + + static HashMap fromPricingPhase( + @Nullable ProductDetails.PricingPhase pricingPhase) { + HashMap serialized = new HashMap<>(); + + if (pricingPhase == null) { + return serialized; + } + + serialized.put("formattedPrice", pricingPhase.getFormattedPrice()); + serialized.put("priceCurrencyCode", pricingPhase.getPriceCurrencyCode()); + serialized.put("priceAmountMicros", pricingPhase.getPriceAmountMicros()); + serialized.put("billingCycleCount", pricingPhase.getBillingCycleCount()); + serialized.put("billingPeriod", pricingPhase.getBillingPeriod()); + serialized.put("recurrenceMode", pricingPhase.getRecurrenceMode()); + + return serialized; + } + static HashMap fromPurchase(Purchase purchase) { HashMap info = new HashMap<>(); - // TODO(stuartmorgan): Migrate this code. See TODO on MethodCallHandlerImpl.querySkuDetailsAsync. - @SuppressWarnings("deprecation") - List skus = purchase.getSkus(); + List products = purchase.getProducts(); info.put("orderId", purchase.getOrderId()); info.put("packageName", purchase.getPackageName()); info.put("purchaseTime", purchase.getPurchaseTime()); info.put("purchaseToken", purchase.getPurchaseToken()); info.put("signature", purchase.getSignature()); - info.put("skus", skus); + info.put("products", products); info.put("isAutoRenewing", purchase.isAutoRenewing()); info.put("originalJson", purchase.getOriginalJson()); info.put("developerPayload", purchase.getDeveloperPayload()); @@ -84,13 +188,11 @@ static HashMap fromPurchase(Purchase purchase) { static HashMap fromPurchaseHistoryRecord( PurchaseHistoryRecord purchaseHistoryRecord) { HashMap info = new HashMap<>(); - // TODO(stuartmorgan): Migrate this code. See TODO on MethodCallHandlerImpl.querySkuDetailsAsync. - @SuppressWarnings("deprecation") - List skus = purchaseHistoryRecord.getSkus(); + List products = purchaseHistoryRecord.getProducts(); info.put("purchaseTime", purchaseHistoryRecord.getPurchaseTime()); info.put("purchaseToken", purchaseHistoryRecord.getPurchaseToken()); info.put("signature", purchaseHistoryRecord.getSignature()); - info.put("skus", skus); + info.put("products", products); info.put("developerPayload", purchaseHistoryRecord.getDeveloperPayload()); info.put("originalJson", purchaseHistoryRecord.getOriginalJson()); info.put("quantity", purchaseHistoryRecord.getQuantity()); diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index 7c4498a30eef..585ed646b441 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -10,17 +10,16 @@ import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.IS_FEATURE_SUPPORTED; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.IS_READY; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW; -import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_DISCONNECT; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_PURCHASES_UPDATED; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PRODUCT_DETAILS; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASES_ASYNC; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC; -import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_SKU_DETAILS; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.START_CONNECTION; import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; +import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; -import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.Collections.unmodifiableList; @@ -51,23 +50,27 @@ import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.ConsumeResponseListener; +import com.android.billingclient.api.ProductDetails; +import com.android.billingclient.api.ProductDetailsResponseListener; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.PurchaseHistoryRecord; import com.android.billingclient.api.PurchaseHistoryResponseListener; import com.android.billingclient.api.PurchasesResponseListener; +import com.android.billingclient.api.QueryProductDetailsParams; import com.android.billingclient.api.QueryPurchaseHistoryParams; import com.android.billingclient.api.QueryPurchasesParams; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import org.json.JSONException; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -77,10 +80,6 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -// TODO(stuartmorgan): Migrate this code. See TODO on MethodCallHandlerImpl.querySkuDetailsAsync. -// This is supressed at the class level since until the code is migrated, the tests will be -// full of deprecation warnings. -@SuppressWarnings("deprecation") public class MethodCallHandlerTest { private MethodCallHandlerImpl methodChannelHandler; private BillingClientFactory factory; @@ -213,59 +212,56 @@ public void endConnection() { } @Test - public void querySkuDetailsAsync() { - // Connect a billing client and set up the SKU query listeners + public void queryProductDetailsAsync() { + // Connect a billing client and set up the product query listeners establishConnectedBillingClient(/* arguments= */ null, /* result= */ null); - String skuType = BillingClient.SkuType.INAPP; - List skusList = asList("id1", "id2"); + String productType = BillingClient.ProductType.INAPP; + List productsList = asList("id1", "id2"); HashMap arguments = new HashMap<>(); - arguments.put("skuType", skuType); - arguments.put("skusList", skusList); - MethodCall queryCall = new MethodCall(QUERY_SKU_DETAILS, arguments); + arguments.put("productList", buildProductMap(productsList, productType)); + MethodCall queryCall = new MethodCall(QUERY_PRODUCT_DETAILS, arguments); - // Query for SKU details + // Query for product details methodChannelHandler.onMethodCall(queryCall, result); // Assert the arguments were forwarded correctly to BillingClient - ArgumentCaptor paramCaptor = - ArgumentCaptor.forClass(com.android.billingclient.api.SkuDetailsParams.class); - ArgumentCaptor listenerCaptor = - ArgumentCaptor.forClass(com.android.billingclient.api.SkuDetailsResponseListener.class); - verify(mockBillingClient).querySkuDetailsAsync(paramCaptor.capture(), listenerCaptor.capture()); - assertEquals(paramCaptor.getValue().getSkuType(), skuType); - assertEquals(paramCaptor.getValue().getSkusList(), skusList); + ArgumentCaptor paramCaptor = + ArgumentCaptor.forClass(QueryProductDetailsParams.class); + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(ProductDetailsResponseListener.class); + verify(mockBillingClient) + .queryProductDetailsAsync(paramCaptor.capture(), listenerCaptor.capture()); // Assert that we handed result BillingClient's response int responseCode = 200; - List skuDetailsResponse = - asList(buildSkuDetails("foo")); + List productDetailsResponse = asList(buildProductDetails("foo")); BillingResult billingResult = BillingResult.newBuilder() .setResponseCode(100) .setDebugMessage("dummy debug message") .build(); - listenerCaptor.getValue().onSkuDetailsResponse(billingResult, skuDetailsResponse); + listenerCaptor.getValue().onProductDetailsResponse(billingResult, productDetailsResponse); @SuppressWarnings("unchecked") ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); verify(result).success(resultCaptor.capture()); HashMap resultData = resultCaptor.getValue(); assertEquals(resultData.get("billingResult"), fromBillingResult(billingResult)); - assertEquals(resultData.get("skuDetailsList"), fromSkuDetailsList(skuDetailsResponse)); + assertEquals( + resultData.get("productDetailsList"), fromProductDetailsList(productDetailsResponse)); } @Test - public void querySkuDetailsAsync_clientDisconnected() { - // Disconnect the Billing client and prepare a querySkuDetails call + public void queryProductDetailsAsync_clientDisconnected() { + // Disconnect the Billing client and prepare a queryProductDetails call MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); - String skuType = BillingClient.SkuType.INAPP; - List skusList = asList("id1", "id2"); + String productType = BillingClient.ProductType.INAPP; + List productsList = asList("id1", "id2"); HashMap arguments = new HashMap<>(); - arguments.put("skuType", skuType); - arguments.put("skusList", skusList); - MethodCall queryCall = new MethodCall(QUERY_SKU_DETAILS, arguments); + arguments.put("productList", buildProductMap(productsList, productType)); + MethodCall queryCall = new MethodCall(QUERY_PRODUCT_DETAILS, arguments); - // Query for SKU details + // Query for product details methodChannelHandler.onMethodCall(queryCall, result); // Assert that we sent an error back. @@ -278,11 +274,11 @@ public void querySkuDetailsAsync_clientDisconnected() { // since PBL 3.0, the `accountId` variable is not public. @Test public void launchBillingFlow_null_AccountId_do_not_crash() { - // Fetch the sku details first and then prepare the launch billing flow call - String skuId = "foo"; - queryForSkus(singletonList(skuId)); + // Fetch the product details first and then prepare the launch billing flow call + String productId = "foo"; + queryForProducts(singletonList(productId)); HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); + arguments.put("product", productId); arguments.put("accountId", null); arguments.put("obfuscatedProfileId", null); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); @@ -308,15 +304,15 @@ public void launchBillingFlow_null_AccountId_do_not_crash() { } @Test - public void launchBillingFlow_ok_null_OldSku() { - // Fetch the sku details first and then prepare the launch billing flow call - String skuId = "foo"; + public void launchBillingFlow_ok_null_OldProduct() { + // Fetch the product details first and then prepare the launch billing flow call + String productId = "foo"; String accountId = "account"; - queryForSkus(singletonList(skuId)); + queryForProducts(singletonList(productId)); HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); + arguments.put("product", productId); arguments.put("accountId", accountId); - arguments.put("oldSku", null); + arguments.put("oldProduct", null); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); // Launch the billing flow @@ -342,12 +338,12 @@ public void launchBillingFlow_ok_null_OldSku() { public void launchBillingFlow_ok_null_Activity() { methodChannelHandler.setActivity(null); - // Fetch the sku details first and then prepare the launch billing flow call - String skuId = "foo"; + // Fetch the product details first and then prepare the launch billing flow call + String productId = "foo"; String accountId = "account"; - queryForSkus(singletonList(skuId)); + queryForProducts(singletonList(productId)); HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); + arguments.put("product", productId); arguments.put("accountId", accountId); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); methodChannelHandler.onMethodCall(launchCall, result); @@ -358,16 +354,16 @@ public void launchBillingFlow_ok_null_Activity() { } @Test - public void launchBillingFlow_ok_oldSku() { - // Fetch the sku details first and query the method call - String skuId = "foo"; + public void launchBillingFlow_ok_oldProduct() { + // Fetch the product details first and query the method call + String productId = "foo"; String accountId = "account"; - String oldSkuId = "oldFoo"; - queryForSkus(unmodifiableList(asList(skuId, oldSkuId))); + String oldProductId = "oldFoo"; + queryForProducts(unmodifiableList(asList(productId, oldProductId))); HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); + arguments.put("product", productId); arguments.put("accountId", accountId); - arguments.put("oldSku", oldSkuId); + arguments.put("oldProduct", oldProductId); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); // Launch the billing flow @@ -392,12 +388,12 @@ public void launchBillingFlow_ok_oldSku() { @Test public void launchBillingFlow_ok_AccountId() { - // Fetch the sku details first and query the method call - String skuId = "foo"; + // Fetch the product details first and query the method call + String productId = "foo"; String accountId = "account"; - queryForSkus(singletonList(skuId)); + queryForProducts(singletonList(productId)); HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); + arguments.put("product", productId); arguments.put("accountId", accountId); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); @@ -423,17 +419,17 @@ public void launchBillingFlow_ok_AccountId() { @Test public void launchBillingFlow_ok_Proration() { - // Fetch the sku details first and query the method call - String skuId = "foo"; - String oldSkuId = "oldFoo"; + // Fetch the product details first and query the method call + String productId = "foo"; + String oldProductId = "oldFoo"; String purchaseToken = "purchaseTokenFoo"; String accountId = "account"; int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE; - queryForSkus(unmodifiableList(asList(skuId, oldSkuId))); + queryForProducts(unmodifiableList(asList(productId, oldProductId))); HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); + arguments.put("product", productId); arguments.put("accountId", accountId); - arguments.put("oldSku", oldSkuId); + arguments.put("oldProduct", oldProductId); arguments.put("purchaseToken", purchaseToken); arguments.put("prorationMode", prorationMode); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); @@ -459,18 +455,18 @@ public void launchBillingFlow_ok_Proration() { } @Test - public void launchBillingFlow_ok_Proration_with_null_OldSku() { - // Fetch the sku details first and query the method call - String skuId = "foo"; + public void launchBillingFlow_ok_Proration_with_null_OldProduct() { + // Fetch the product details first and query the method call + String productId = "foo"; String accountId = "account"; - String queryOldSkuId = "oldFoo"; - String oldSkuId = null; + String queryOldProductId = "oldFoo"; + String oldProductId = null; int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE; - queryForSkus(unmodifiableList(asList(skuId, queryOldSkuId))); + queryForProducts(unmodifiableList(asList(productId, queryOldProductId))); HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); + arguments.put("product", productId); arguments.put("accountId", accountId); - arguments.put("oldSku", oldSkuId); + arguments.put("oldProduct", oldProductId); arguments.put("prorationMode", prorationMode); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); @@ -486,25 +482,25 @@ public void launchBillingFlow_ok_Proration_with_null_OldSku() { // Assert that we sent an error back. verify(result) .error( - contains("IN_APP_PURCHASE_REQUIRE_OLD_SKU"), - contains("launchBillingFlow failed because oldSku is null"), + contains("IN_APP_PURCHASE_REQUIRE_OLD_PRODUCT"), + contains("launchBillingFlow failed because oldProduct is null"), any()); verify(result, never()).success(any()); } @Test public void launchBillingFlow_ok_Full() { - // Fetch the sku details first and query the method call - String skuId = "foo"; - String oldSkuId = "oldFoo"; + // Fetch the product details first and query the method call + String productId = "foo"; + String oldProductId = "oldFoo"; String purchaseToken = "purchaseTokenFoo"; String accountId = "account"; int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE; - queryForSkus(unmodifiableList(asList(skuId, oldSkuId))); + queryForProducts(unmodifiableList(asList(productId, oldProductId))); HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); + arguments.put("product", productId); arguments.put("accountId", accountId); - arguments.put("oldSku", oldSkuId); + arguments.put("oldProduct", oldProductId); arguments.put("purchaseToken", purchaseToken); arguments.put("prorationMode", prorationMode); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); @@ -534,10 +530,10 @@ public void launchBillingFlow_clientDisconnected() { // Prepare the launch call after disconnecting the client MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); - String skuId = "foo"; + String productId = "foo"; String accountId = "account"; HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); + arguments.put("product", productId); arguments.put("accountId", accountId); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); @@ -549,41 +545,42 @@ public void launchBillingFlow_clientDisconnected() { } @Test - public void launchBillingFlow_skuNotFound() { - // Try to launch the billing flow for a random sku ID + public void launchBillingFlow_productNotFound() { + // Try to launch the billing flow for a random product ID establishConnectedBillingClient(null, null); - String skuId = "foo"; + String productId = "foo"; String accountId = "account"; HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); + arguments.put("product", productId); arguments.put("accountId", accountId); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); methodChannelHandler.onMethodCall(launchCall, result); // Assert that we sent an error back. - verify(result).error(contains("NOT_FOUND"), contains(skuId), any()); + verify(result).error(contains("NOT_FOUND"), contains(productId), any()); verify(result, never()).success(any()); } @Test - public void launchBillingFlow_oldSkuNotFound() { - // Try to launch the billing flow for a random sku ID + public void launchBillingFlow_oldProductNotFound() { + // Try to launch the billing flow for a random product ID establishConnectedBillingClient(null, null); - String skuId = "foo"; + String productId = "foo"; String accountId = "account"; - String oldSkuId = "oldSku"; - queryForSkus(singletonList(skuId)); + String oldProductId = "oldProduct"; + queryForProducts(singletonList(productId)); HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); + arguments.put("product", productId); arguments.put("accountId", accountId); - arguments.put("oldSku", oldSkuId); + arguments.put("oldProduct", oldProductId); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); methodChannelHandler.onMethodCall(launchCall, result); // Assert that we sent an error back. - verify(result).error(contains("IN_APP_PURCHASE_INVALID_OLD_SKU"), contains(oldSkuId), any()); + verify(result) + .error(contains("IN_APP_PURCHASE_INVALID_OLD_PRODUCT"), contains(oldProductId), any()); verify(result, never()).success(any()); } @@ -593,7 +590,7 @@ public void queryPurchases_clientDisconnected() { methodChannelHandler.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class)); HashMap arguments = new HashMap<>(); - arguments.put("skuType", BillingClient.SkuType.INAPP); + arguments.put("type", BillingClient.ProductType.INAPP); methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES_ASYNC, arguments), result); // Assert that we sent an error back. @@ -636,7 +633,7 @@ public Object answer(InvocationOnMock invocation) { any(QueryPurchasesParams.class), purchasesResponseListenerArgumentCaptor.capture()); HashMap arguments = new HashMap<>(); - arguments.put("skuType", BillingClient.SkuType.INAPP); + arguments.put("productType", BillingClient.ProductType.INAPP); methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES_ASYNC, arguments), result); lock.await(5000, TimeUnit.MILLISECONDS); @@ -667,7 +664,7 @@ public void queryPurchaseHistoryAsync() { .build(); List purchasesList = asList(buildPurchaseHistoryRecord("foo")); HashMap arguments = new HashMap<>(); - arguments.put("skuType", BillingClient.SkuType.INAPP); + arguments.put("productType", BillingClient.ProductType.INAPP); ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(PurchaseHistoryResponseListener.class); @@ -691,7 +688,7 @@ public void queryPurchaseHistoryAsync_clientDisconnected() { methodChannelHandler.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class)); HashMap arguments = new HashMap<>(); - arguments.put("skuType", BillingClient.SkuType.INAPP); + arguments.put("type", BillingClient.ProductType.INAPP); methodChannelHandler.onMethodCall( new MethodCall(QUERY_PURCHASE_HISTORY_ASYNC, arguments), result); @@ -830,102 +827,6 @@ public void isFutureSupported_false() { verify(result).success(false); } - @Test - public void launchPriceChangeConfirmationFlow() { - // Set up the sku details - establishConnectedBillingClient(null, null); - String skuId = "foo"; - queryForSkus(singletonList(skuId)); - - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(BillingClient.BillingResponseCode.OK) - .setDebugMessage("dummy debug message") - .build(); - - // Set up the mock billing client - ArgumentCaptor - priceChangeConfirmationListenerArgumentCaptor = - ArgumentCaptor.forClass( - com.android.billingclient.api.PriceChangeConfirmationListener.class); - ArgumentCaptor - priceChangeFlowParamsArgumentCaptor = - ArgumentCaptor.forClass(com.android.billingclient.api.PriceChangeFlowParams.class); - doNothing() - .when(mockBillingClient) - .launchPriceChangeConfirmationFlow( - any(), - priceChangeFlowParamsArgumentCaptor.capture(), - priceChangeConfirmationListenerArgumentCaptor.capture()); - - // Call the methodChannelHandler - HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); - methodChannelHandler.onMethodCall( - new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result); - - // Verify the price change params. - com.android.billingclient.api.PriceChangeFlowParams priceChangeFlowParams = - priceChangeFlowParamsArgumentCaptor.getValue(); - assertEquals(skuId, priceChangeFlowParams.getSkuDetails().getSku()); - - // Set the response in the callback - com.android.billingclient.api.PriceChangeConfirmationListener priceChangeConfirmationListener = - priceChangeConfirmationListenerArgumentCaptor.getValue(); - priceChangeConfirmationListener.onPriceChangeConfirmationResult(billingResult); - - // Verify we pass the response to result - verify(result, never()).error(any(), any(), any()); - @SuppressWarnings("unchecked") - ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); - verify(result, times(1)).success(resultCaptor.capture()); - assertEquals(fromBillingResult(billingResult), resultCaptor.getValue()); - } - - @Test - public void launchPriceChangeConfirmationFlow_withoutActivity_returnsActivityUnavailableError() { - // Set up the sku details - establishConnectedBillingClient(null, null); - String skuId = "foo"; - queryForSkus(singletonList(skuId)); - - methodChannelHandler.setActivity(null); - - // Call the methodChannelHandler - HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); - methodChannelHandler.onMethodCall( - new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result); - verify(result, times(1)).error(eq("ACTIVITY_UNAVAILABLE"), any(), any()); - } - - @Test - public void launchPriceChangeConfirmationFlow_withoutSkuQuery_returnsNotFoundError() { - // Set up the sku details - establishConnectedBillingClient(null, null); - String skuId = "foo"; - - // Call the methodChannelHandler - HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); - methodChannelHandler.onMethodCall( - new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result); - verify(result, times(1)).error(eq("NOT_FOUND"), contains("sku"), any()); - } - - @Test - public void launchPriceChangeConfirmationFlow_withoutBillingClient_returnsUnavailableError() { - // Set up the sku details - String skuId = "foo"; - - // Call the methodChannelHandler - HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); - methodChannelHandler.onMethodCall( - new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result); - verify(result, times(1)).error(eq("UNAVAILABLE"), contains("BillingClient"), any()); - } - private ArgumentCaptor mockStartConnection() { Map arguments = new HashMap<>(); arguments.put("handle", 1); @@ -952,45 +853,65 @@ private void establishConnectedBillingClient( methodChannelHandler.onMethodCall(connectCall, result); } - private void queryForSkus(List skusList) { + private void queryForProducts(List productIdList) { // Set up the query method call establishConnectedBillingClient(/* arguments= */ null, /* result= */ null); HashMap arguments = new HashMap<>(); - String skuType = BillingClient.SkuType.INAPP; - arguments.put("skuType", skuType); - arguments.put("skusList", skusList); - MethodCall queryCall = new MethodCall(QUERY_SKU_DETAILS, arguments); + String productType = BillingClient.ProductType.INAPP; + List> productList = buildProductMap(productIdList, productType); + arguments.put("productList", productList); + MethodCall queryCall = new MethodCall(QUERY_PRODUCT_DETAILS, arguments); // Call the method. methodChannelHandler.onMethodCall(queryCall, mock(Result.class)); - // Respond to the call with a matching set of Sku details. - ArgumentCaptor listenerCaptor = - ArgumentCaptor.forClass(com.android.billingclient.api.SkuDetailsResponseListener.class); - verify(mockBillingClient).querySkuDetailsAsync(any(), listenerCaptor.capture()); - List skuDetailsResponse = - skusList.stream().map(this::buildSkuDetails).collect(toList()); + // Respond to the call with a matching set of product details. + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(ProductDetailsResponseListener.class); + verify(mockBillingClient).queryProductDetailsAsync(any(), listenerCaptor.capture()); + List productDetailsResponse = + productIdList.stream().map(this::buildProductDetails).collect(toList()); BillingResult billingResult = BillingResult.newBuilder() .setResponseCode(100) .setDebugMessage("dummy debug message") .build(); - listenerCaptor.getValue().onSkuDetailsResponse(billingResult, skuDetailsResponse); + listenerCaptor.getValue().onProductDetailsResponse(billingResult, productDetailsResponse); + } + + private List> buildProductMap(List productIds, String productType) { + List> productList = new ArrayList<>(); + for (String productId : productIds) { + Map productMap = new HashMap<>(); + productMap.put("productId", productId); + productMap.put("productType", productType); + productList.add(productMap); + } + return productList; } - private com.android.billingclient.api.SkuDetails buildSkuDetails(String id) { + private ProductDetails buildProductDetails(String id) { String json = String.format( - "{\"packageName\": \"dummyPackageName\",\"productId\":\"%s\",\"type\":\"inapp\",\"price\":\"$0.99\",\"price_amount_micros\":990000,\"price_currency_code\":\"USD\",\"title\":\"Example title\",\"description\":\"Example description.\",\"original_price\":\"$0.99\",\"original_price_micros\":990000}", + "{\"title\":\"Example title\",\"description\":\"Example description\",\"productId\":\"%s\",\"type\":\"inapp\",\"name\":\"Example name\",\"oneTimePurchaseOfferDetails\":{\"priceAmountMicros\":990000,\"priceCurrencyCode\":\"USD\",\"formattedPrice\":\"$0.99\"}}", id); - com.android.billingclient.api.SkuDetails details = null; + try { - details = new com.android.billingclient.api.SkuDetails(json); - } catch (JSONException e) { - fail("buildSkuDetails failed with JSONException " + e.toString()); + Constructor productDetailsConstructor = + ProductDetails.class.getDeclaredConstructor(String.class); + productDetailsConstructor.setAccessible(true); + return productDetailsConstructor.newInstance(json); + } catch (NoSuchMethodException e) { + fail("buildProductDetails failed with NoSuchMethodException " + e); + } catch (InvocationTargetException e) { + fail("buildProductDetails failed with InvocationTargetException " + e); + } catch (IllegalAccessException e) { + fail("buildProductDetails failed with IllegalAccessException " + e); + } catch (InstantiationException e) { + fail("buildProductDetails failed with InstantiationException " + e); } - return details; + return null; } private Purchase buildPurchase(String orderId) { diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java index 914ef0b57efa..aa32afe2e43c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java @@ -14,8 +14,11 @@ import com.android.billingclient.api.AccountIdentifiers; import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.ProductDetails; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.PurchaseHistoryRecord; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -26,42 +29,56 @@ import org.junit.Before; import org.junit.Test; -// TODO(stuartmorgan): Migrate this code. See TODO on MethodCallHandlerImpl.querySkuDetailsAsync. -// This is supressed at the class level since until the code is migrated, the tests will be -// full of deprecation warnings. -@SuppressWarnings("deprecation") public class TranslatorTest { - private static final String SKU_DETAIL_EXAMPLE_JSON = - "{\"productId\":\"example\",\"type\":\"inapp\",\"price\":\"$0.99\",\"price_amount_micros\":990000,\"price_currency_code\":\"USD\",\"title\":\"Example title\",\"description\":\"Example description.\",\"original_price\":\"$0.99\",\"original_price_micros\":990000}"; private static final String PURCHASE_EXAMPLE_JSON = - "{\"orderId\":\"foo\",\"packageName\":\"bar\",\"productId\":\"consumable\",\"purchaseTime\":11111111,\"purchaseState\":0,\"purchaseToken\":\"baz\",\"developerPayload\":\"dummy payload\",\"isAcknowledged\":\"true\", \"obfuscatedAccountId\":\"Account101\", \"obfuscatedProfileId\": \"Profile105\"}"; + "{\"orderId\":\"foo\",\"packageName\":\"bar\",\"productId\":\"consumable\",\"purchaseTime\":11111111,\"purchaseState\":0,\"purchaseToken\":\"baz\",\"developerPayload\":\"dummy payload\",\"isAcknowledged\":\"true\", \"obfuscatedAccountId\":\"Account101\", \"obfuscatedProfileId\":\"Profile105\"}"; + private static final String IN_APP_PRODUCT_DETAIL_EXAMPLE_JSON = + "{\"title\":\"Example title\",\"description\":\"Example description\",\"productId\":\"Example id\",\"type\":\"inapp\",\"name\":\"Example name\",\"oneTimePurchaseOfferDetails\":{\"priceAmountMicros\":990000,\"priceCurrencyCode\":\"USD\",\"formattedPrice\":\"$0.99\"}}"; + private static final String SUBS_PRODUCT_DETAIL_EXAMPLE_JSON = + "{\"title\":\"Example title 2\",\"description\":\"Example description 2\",\"productId\":\"Example id 2\",\"type\":\"subs\",\"name\":\"Example name 2\",\"subscriptionOfferDetails\":[{\"offerId\":\"Example offer id\",\"basePlanId\":\"Example base plan id\",\"offerTags\":[\"Example offer tag\"],\"offerIdToken\":\"Example offer token\",\"pricingPhases\":[{\"formattedPrice\":\"$0.99\",\"priceCurrencyCode\":\"USD\",\"priceAmountMicros\":990000,\"billingCycleCount\":4,\"billingPeriod\":\"Example billing period\",\"recurrenceMode\":0}]}]}"; + + Constructor productDetailsConstructor; @Before - public void setup() { + public void setup() throws NoSuchMethodException { Locale locale = new Locale("en", "us"); Locale.setDefault(locale); + + productDetailsConstructor = ProductDetails.class.getDeclaredConstructor(String.class); + productDetailsConstructor.setAccessible(true); } @Test - public void fromSkuDetail() throws JSONException { - final com.android.billingclient.api.SkuDetails expected = - new com.android.billingclient.api.SkuDetails(SKU_DETAIL_EXAMPLE_JSON); + public void fromInAppProductDetail() + throws InvocationTargetException, IllegalAccessException, InstantiationException { + final ProductDetails expected = + productDetailsConstructor.newInstance(IN_APP_PRODUCT_DETAIL_EXAMPLE_JSON); - Map serialized = Translator.fromSkuDetail(expected); + Map serialized = Translator.fromProductDetail(expected); assertSerialized(expected, serialized); } @Test - public void fromSkuDetailsList() throws JSONException { - final String SKU_DETAIL_EXAMPLE_2_JSON = - "{\"productId\":\"example2\",\"type\":\"inapp\",\"price\":\"$0.99\",\"price_amount_micros\":990000,\"price_currency_code\":\"USD\",\"title\":\"Example title\",\"description\":\"Example description.\",\"original_price\":\"$0.99\",\"original_price_micros\":990000}"; - final List expected = + public void fromSubsProductDetail() + throws InvocationTargetException, IllegalAccessException, InstantiationException { + final ProductDetails expected = + productDetailsConstructor.newInstance(SUBS_PRODUCT_DETAIL_EXAMPLE_JSON); + + Map serialized = Translator.fromProductDetail(expected); + + assertSerialized(expected, serialized); + } + + @Test + public void fromProductDetailsList() + throws InvocationTargetException, IllegalAccessException, InstantiationException { + final List expected = Arrays.asList( - new com.android.billingclient.api.SkuDetails(SKU_DETAIL_EXAMPLE_JSON), - new com.android.billingclient.api.SkuDetails(SKU_DETAIL_EXAMPLE_2_JSON)); + productDetailsConstructor.newInstance(IN_APP_PRODUCT_DETAIL_EXAMPLE_JSON), + productDetailsConstructor.newInstance(SUBS_PRODUCT_DETAIL_EXAMPLE_JSON)); - final List> serialized = Translator.fromSkuDetailsList(expected); + final List> serialized = Translator.fromProductDetailsList(expected); assertEquals(expected.size(), serialized.size()); assertSerialized(expected.get(0), serialized.get(0)); @@ -69,8 +86,8 @@ public void fromSkuDetailsList() throws JSONException { } @Test - public void fromSkuDetailsList_null() { - assertEquals(Collections.emptyList(), Translator.fromSkuDetailsList(null)); + public void fromProductDetailsList_null() { + assertEquals(Collections.emptyList(), Translator.fromProductDetailsList(null)); } @Test @@ -141,7 +158,7 @@ public void fromPurchasesList_null() { } @Test - public void fromBillingResult() throws JSONException { + public void fromBillingResult() { BillingResult newBillingResult = BillingResult.newBuilder() .setDebugMessage("dummy debug message") @@ -154,7 +171,7 @@ public void fromBillingResult() throws JSONException { } @Test - public void fromBillingResult_debugMessageNull() throws JSONException { + public void fromBillingResult_debugMessageNull() { BillingResult newBillingResult = BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK).build(); Map billingResultMap = Translator.fromBillingResult(newBillingResult); @@ -174,27 +191,88 @@ public void currencyCodeFromSymbol() { } } - private void assertSerialized( - com.android.billingclient.api.SkuDetails expected, Map serialized) { + private void assertSerialized(ProductDetails expected, Map serialized) { assertEquals(expected.getDescription(), serialized.get("description")); - assertEquals(expected.getFreeTrialPeriod(), serialized.get("freeTrialPeriod")); - assertEquals(expected.getIntroductoryPrice(), serialized.get("introductoryPrice")); + assertEquals(expected.getTitle(), serialized.get("title")); + assertEquals(expected.getName(), serialized.get("name")); + assertEquals(expected.getProductId(), serialized.get("productId")); + assertEquals(expected.getProductType(), serialized.get("productType")); + + ProductDetails.OneTimePurchaseOfferDetails expectedOneTimePurchaseOfferDetails = + expected.getOneTimePurchaseOfferDetails(); + Object oneTimePurchaseOfferDetailsObject = serialized.get("oneTimePurchaseOfferDetails"); + assertEquals( + expectedOneTimePurchaseOfferDetails == null, oneTimePurchaseOfferDetailsObject == null); + if (expectedOneTimePurchaseOfferDetails != null && oneTimePurchaseOfferDetailsObject != null) { + @SuppressWarnings(value = "unchecked") + Map oneTimePurchaseOfferDetailsMap = + (Map) oneTimePurchaseOfferDetailsObject; + assertSerialized(expectedOneTimePurchaseOfferDetails, oneTimePurchaseOfferDetailsMap); + } + + List expectedSubscriptionOfferDetailsList = + expected.getSubscriptionOfferDetails(); + Object subscriptionOfferDetailsListObject = serialized.get("subscriptionOfferDetails"); assertEquals( - expected.getIntroductoryPriceAmountMicros(), - serialized.get("introductoryPriceAmountMicros")); - assertEquals(expected.getIntroductoryPriceCycles(), serialized.get("introductoryPriceCycles")); - assertEquals(expected.getIntroductoryPricePeriod(), serialized.get("introductoryPricePeriod")); - assertEquals(expected.getPrice(), serialized.get("price")); + expectedSubscriptionOfferDetailsList == null, subscriptionOfferDetailsListObject == null); + if (expectedSubscriptionOfferDetailsList != null + && subscriptionOfferDetailsListObject != null) { + @SuppressWarnings(value = "unchecked") + List subscriptionOfferDetailsListList = + (List) subscriptionOfferDetailsListObject; + assertSerialized(expectedSubscriptionOfferDetailsList, subscriptionOfferDetailsListList); + } + } + + private void assertSerialized( + ProductDetails.OneTimePurchaseOfferDetails expected, Map serialized) { assertEquals(expected.getPriceAmountMicros(), serialized.get("priceAmountMicros")); assertEquals(expected.getPriceCurrencyCode(), serialized.get("priceCurrencyCode")); - assertEquals("$", serialized.get("priceCurrencySymbol")); - assertEquals(expected.getSku(), serialized.get("sku")); - assertEquals(expected.getSubscriptionPeriod(), serialized.get("subscriptionPeriod")); - assertEquals(expected.getTitle(), serialized.get("title")); - assertEquals(expected.getType(), serialized.get("type")); - assertEquals(expected.getOriginalPrice(), serialized.get("originalPrice")); - assertEquals( - expected.getOriginalPriceAmountMicros(), serialized.get("originalPriceAmountMicros")); + assertEquals(expected.getFormattedPrice(), serialized.get("formattedPrice")); + } + + private void assertSerialized( + List expected, List serialized) { + assertEquals(expected.size(), serialized.size()); + for (int i = 0; i < expected.size(); i++) { + @SuppressWarnings(value = "unchecked") + Map serializedMap = (Map) serialized.get(i); + assertSerialized(expected.get(i), serializedMap); + } + } + + private void assertSerialized( + ProductDetails.SubscriptionOfferDetails expected, Map serialized) { + assertEquals(expected.getBasePlanId(), serialized.get("basePlanId")); + assertEquals(expected.getOfferId(), serialized.get("offerId")); + assertEquals(expected.getOfferTags(), serialized.get("offerTags")); + assertEquals(expected.getOfferToken(), serialized.get("offerIdToken")); + + @SuppressWarnings(value = "unchecked") + List serializedPricingPhases = (List) serialized.get("pricingPhases"); + assertNotNull(serializedPricingPhases); + assertSerialized(expected.getPricingPhases(), serializedPricingPhases); + } + + private void assertSerialized(ProductDetails.PricingPhases expected, List serialized) { + List expectedPhases = expected.getPricingPhaseList(); + assertEquals(expectedPhases.size(), serialized.size()); + for (int i = 0; i < serialized.size(); i++) { + @SuppressWarnings(value = "unchecked") + Map pricingPhaseMap = (Map) serialized.get(i); + assertSerialized(expectedPhases.get(i), pricingPhaseMap); + } + expected.getPricingPhaseList(); + } + + private void assertSerialized( + ProductDetails.PricingPhase expected, Map serialized) { + assertEquals(expected.getFormattedPrice(), serialized.get("formattedPrice")); + assertEquals(expected.getPriceCurrencyCode(), serialized.get("priceCurrencyCode")); + assertEquals(expected.getPriceAmountMicros(), serialized.get("priceAmountMicros")); + assertEquals(expected.getBillingCycleCount(), serialized.get("billingCycleCount")); + assertEquals(expected.getBillingPeriod(), serialized.get("billingPeriod")); + assertEquals(expected.getRecurrenceMode(), serialized.get("recurrenceMode")); } private void assertSerialized(Purchase expected, Map serialized) { @@ -204,7 +282,7 @@ private void assertSerialized(Purchase expected, Map serialized) assertEquals(expected.getPurchaseToken(), serialized.get("purchaseToken")); assertEquals(expected.getSignature(), serialized.get("signature")); assertEquals(expected.getOriginalJson(), serialized.get("originalJson")); - assertEquals(expected.getSkus(), serialized.get("skus")); + assertEquals(expected.getProducts(), serialized.get("products")); assertEquals(expected.getDeveloperPayload(), serialized.get("developerPayload")); assertEquals(expected.isAcknowledged(), serialized.get("isAcknowledged")); assertEquals(expected.getPurchaseState(), serialized.get("purchaseState")); @@ -223,7 +301,7 @@ private void assertSerialized(PurchaseHistoryRecord expected, Map[]); + } on MissingPluginException { + fail('Method channel is not setup correctly'); + } on PlatformException catch (e) { + // A [PlatformException] is expected, as we send an empty product list. + if (!(e.message?.startsWith('Product list cannot be empty.') ?? + false)) { + rethrow; + } + } + }); + + test('BillingClient.queryPurchaseHistory', () async { + try { + await billingClient.queryPurchaseHistory(ProductType.inapp); + } on MissingPluginException { + fail('Method channel is not setup correctly'); + } + }); + + test('BillingClient.queryPurchases', () async { + try { + await billingClient.queryPurchases(ProductType.inapp); + } on MissingPluginException { + fail('Method channel is not setup correctly'); + } + }); + + test('BillingClient.startConnection', () async { + try { + await billingClient.startConnection( + onBillingServiceDisconnected: () {}); + } on MissingPluginException { + fail('Method channel is not setup correctly'); + } + }); + }); } diff --git a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart index bd1955526654..7039d1b123cc 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart @@ -254,21 +254,7 @@ class _MyAppState extends State<_MyApp> { productDetails.description, ), trailing: previousPurchase != null - ? IconButton( - onPressed: () { - final InAppPurchaseAndroidPlatformAddition addition = - InAppPurchasePlatformAddition.instance! - as InAppPurchaseAndroidPlatformAddition; - final SkuDetailsWrapper skuDetails = - (productDetails as GooglePlayProductDetails) - .skuDetails; - addition - .launchPriceChangeConfirmationFlow( - sku: skuDetails.sku) - .then((BillingResultWrapper value) => print( - 'confirmationResponse: ${value.responseCode}')); - }, - icon: const Icon(Icons.upgrade)) + ? const SizedBox.shrink() : TextButton( style: TextButton.styleFrom( backgroundColor: Colors.green[800], @@ -503,6 +489,8 @@ class _FeatureCard extends StatelessWidget { return 'inAppItemsOnVR'; case BillingClientFeature.priceChangeConfirmation: return 'priceChangeConfirmation'; + case BillingClientFeature.productDetails: + return 'productDetails'; case BillingClientFeature.subscriptions: return 'subscriptions'; case BillingClientFeature.subscriptionsOnVR: diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart b/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart index b49be8fe0fe1..31133424afb6 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart @@ -4,5 +4,9 @@ export 'src/billing_client_wrappers/billing_client_manager.dart'; export 'src/billing_client_wrappers/billing_client_wrapper.dart'; +export 'src/billing_client_wrappers/billing_response_wrapper.dart'; +export 'src/billing_client_wrappers/one_time_purchase_offer_details_wrapper.dart'; +export 'src/billing_client_wrappers/product_details_wrapper.dart'; +export 'src/billing_client_wrappers/product_wrapper.dart'; export 'src/billing_client_wrappers/purchase_wrapper.dart'; -export 'src/billing_client_wrappers/sku_details_wrapper.dart'; +export 'src/billing_client_wrappers/subscription_offer_details_wrapper.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart index 04a73f6c5645..8132c5b359da 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart @@ -16,7 +16,7 @@ part 'billing_client_wrapper.g.dart'; /// Method identifier for the OnPurchaseUpdated method channel method. @visibleForTesting const String kOnPurchasesUpdated = - 'PurchasesUpdatedListener#onPurchasesUpdated(int, List)'; + 'PurchasesUpdatedListener#onPurchasesUpdated(BillingResult, List)'; const String _kOnBillingServiceDisconnected = 'BillingClientStateListener#onBillingServiceDisconnected()'; @@ -133,31 +133,34 @@ class BillingClient { return channel.invokeMethod('BillingClient#endConnection()'); } - /// Returns a list of [SkuDetailsWrapper]s that have [SkuDetailsWrapper.sku] - /// in `skusList`, and [SkuDetailsWrapper.type] matching `skuType`. + /// Returns a list of [ProductDetailsResponseWrapper]s that have + /// [ProductDetailsWrapper.productId] and [ProductDetailsWrapper.productType] + /// in `productList`. /// - /// Calls through to [`BillingClient#querySkuDetailsAsync(SkuDetailsParams, - /// SkuDetailsResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querySkuDetailsAsync(com.android.billingclient.api.SkuDetailsParams,%20com.android.billingclient.api.SkuDetailsResponseListener)) + /// Calls through to + /// [`BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryProductDetailsAsync(com.android.billingclient.api.QueryProductDetailsParams,%20com.android.billingclient.api.ProductDetailsResponseListener). /// Instead of taking a callback parameter, it returns a Future - /// [SkuDetailsResponseWrapper]. It also takes the values of - /// `SkuDetailsParams` as direct arguments instead of requiring it constructed - /// and passed in as a class. - Future querySkuDetails( - {required SkuType skuType, required List skusList}) async { + /// [ProductDetailsResponseWrapper]. It also takes the values of + /// `ProductDetailsParams` as direct arguments instead of requiring it + /// constructed and passed in as a class. + Future queryProductDetails({ + required List productList, + }) async { final Map arguments = { - 'skuType': const SkuTypeConverter().toJson(skuType), - 'skusList': skusList + 'productList': + productList.map((ProductWrapper product) => product.toJson()).toList() }; - return SkuDetailsResponseWrapper.fromJson((await channel.invokeMapMethod< - String, dynamic>( - 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)', - arguments)) ?? - {}); + return ProductDetailsResponseWrapper.fromJson( + (await channel.invokeMapMethod( + 'BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)', + arguments, + )) ?? + {}); } - /// Attempt to launch the Play Billing Flow for a given [skuDetails]. + /// Attempt to launch the Play Billing Flow for a given [productDetails]. /// - /// The [skuDetails] needs to have already been fetched in a [querySkuDetails] + /// The [productDetails] needs to have already been fetched in a [queryProductDetails] /// call. The [accountId] is an optional hashed string associated with the user /// that's unique to your app. It's used by Google to detect unusual behavior. /// Do not pass in a cleartext [accountId], and do not use this field to store any Personally Identifiable Information (PII) @@ -179,32 +182,37 @@ class BillingClient { /// [`BillingClient#launchBillingFlow`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#launchbillingflow). /// It constructs a /// [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams) - /// instance by [setting the given skuDetails](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setskudetails), + /// instance by [setting the given productDetails](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setProductDetailsParamsList(java.util.List%3Ccom.android.billingclient.api.BillingFlowParams.ProductDetailsParams%3E)), /// [the given accountId](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedAccountId(java.lang.String)) /// and the [obfuscatedProfileId] (https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setobfuscatedprofileid). /// - /// When this method is called to purchase a subscription, an optional `oldSku` - /// can be passed in. This will tell Google Play that rather than purchasing a new subscription, - /// the user needs to upgrade/downgrade the existing subscription. - /// The [oldSku](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setoldsku) and [purchaseToken] are the SKU id and purchase token that the user is upgrading or downgrading from. - /// [purchaseToken] must not be `null` if [oldSku] is not `null`. - /// The [prorationMode](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setreplaceskusprorationmode) is the mode of proration during subscription upgrade/downgrade. - /// This value will only be effective if the `oldSku` is also set. + /// When this method is called to purchase a subscription through an offer, an + /// [`offerToken` can be passed in](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProductDetailsParams.Builder#setOfferToken(java.lang.String)). + /// + /// When this method is called to purchase a subscription, an optional + /// `oldProduct` can be passed in. This will tell Google Play that rather than + /// purchasing a new subscription, the user needs to upgrade/downgrade the + /// existing subscription. + /// The [oldProduct](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setOldPurchaseToken(java.lang.String)) and [purchaseToken] are the product id and purchase token that the user is upgrading or downgrading from. + /// [purchaseToken] must not be `null` if [oldProduct] is not `null`. + /// The [prorationMode](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setReplaceProrationMode(int)) is the mode of proration during subscription upgrade/downgrade. + /// This value will only be effective if the `oldProduct` is also set. Future launchBillingFlow( - {required String sku, + {required String product, + String? offerToken, String? accountId, String? obfuscatedProfileId, - String? oldSku, + String? oldProduct, String? purchaseToken, ProrationMode? prorationMode}) async { - assert(sku != null); - assert((oldSku == null) == (purchaseToken == null), - 'oldSku and purchaseToken must both be set, or both be null.'); + assert((oldProduct == null) == (purchaseToken == null), + 'oldProduct and purchaseToken must both be set, or both be null.'); final Map arguments = { - 'sku': sku, + 'product': product, + 'offerToken': offerToken, 'accountId': accountId, 'obfuscatedProfileId': obfuscatedProfileId, - 'oldSku': oldSku, + 'oldProduct': oldProduct, 'purchaseToken': purchaseToken, 'prorationMode': const ProrationModeConverter().toJson(prorationMode ?? ProrationMode.unknownSubscriptionUpgradeDowngradePolicy) @@ -216,7 +224,7 @@ class BillingClient { {}); } - /// Fetches recent purchases for the given [SkuType]. + /// Fetches recent purchases for the given [ProductType]. /// /// Unlike [queryPurchaseHistory], This does not make a network request and /// does not return items that are no longer owned. @@ -225,38 +233,38 @@ class BillingClient { /// server if at all possible. See ["Verify a /// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify). /// - /// This wraps [`BillingClient#queryPurchases(String - /// skutype)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchases). - Future queryPurchases(SkuType skuType) async { - assert(skuType != null); - return PurchasesResultWrapper.fromJson((await channel - .invokeMapMethod( - 'BillingClient#queryPurchases(String)', { - 'skuType': const SkuTypeConverter().toJson(skuType) - })) ?? - {}); + /// This wraps + /// [`BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchasesAsync(com.android.billingclient.api.QueryPurchasesParams,%20com.android.billingclient.api.PurchasesResponseListener)). + Future queryPurchases(ProductType productType) async { + return PurchasesResultWrapper.fromJson( + (await channel.invokeMapMethod( + 'BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)', + { + 'productType': const ProductTypeConverter().toJson(productType) + }, + )) ?? + {}); } - /// Fetches purchase history for the given [SkuType]. + /// Fetches purchase history for the given [ProductType]. /// /// Unlike [queryPurchases], this makes a network request via Play and returns - /// the most recent purchase for each [SkuDetailsWrapper] of the given - /// [SkuType] even if the item is no longer owned. + /// the most recent purchase for each [ProductDetailsWrapper] of the given + /// [ProductType] even if the item is no longer owned. /// /// All purchase information should also be verified manually, with your /// server if at all possible. See ["Verify a /// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify). /// - /// This wraps [`BillingClient#queryPurchaseHistoryAsync(String skuType, - /// PurchaseHistoryResponseListener - /// listener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchasehistoryasync). - Future queryPurchaseHistory(SkuType skuType) async { - assert(skuType != null); + /// This wraps + /// [`BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchaseHistoryAsync(com.android.billingclient.api.QueryPurchaseHistoryParams,%20com.android.billingclient.api.PurchaseHistoryResponseListener)). + Future queryPurchaseHistory( + ProductType productType) async { return PurchasesHistoryResult.fromJson((await channel.invokeMapMethod< String, dynamic>( - 'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)', + 'BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)', { - 'skuType': const SkuTypeConverter().toJson(skuType) + 'productType': const ProductTypeConverter().toJson(productType) })) ?? {}); } @@ -266,13 +274,13 @@ class BillingClient { /// Consuming can only be done on an item that's owned, and as a result of consumption, the user will no longer own it. /// Consumption is done asynchronously. The method returns a Future containing a [BillingResultWrapper]. /// - /// This wraps [`BillingClient#consumeAsync(String, ConsumeResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#consumeAsync(java.lang.String,%20com.android.billingclient.api.ConsumeResponseListener)) + /// This wraps + /// [`BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#consumeAsync(java.lang.String,%20com.android.billingclient.api.ConsumeResponseListener)) Future consumeAsync(String purchaseToken) async { - assert(purchaseToken != null); - return BillingResultWrapper.fromJson((await channel - .invokeMapMethod( - 'BillingClient#consumeAsync(String, ConsumeResponseListener)', - { + return BillingResultWrapper.fromJson((await channel.invokeMapMethod( + 'BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)', + { 'purchaseToken': purchaseToken, })) ?? {}); @@ -294,12 +302,12 @@ class BillingClient { /// Please refer to [acknowledge](https://developer.android.com/google/play/billing/billing_library_overview#acknowledge) for more /// details. /// - /// This wraps [`BillingClient#acknowledgePurchase(String, AcknowledgePurchaseResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#acknowledgePurchase(com.android.billingclient.api.AcknowledgePurchaseParams,%20com.android.billingclient.api.AcknowledgePurchaseResponseListener)) + /// This wraps + /// [`BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#acknowledgePurchase(com.android.billingclient.api.AcknowledgePurchaseParams,%20com.android.billingclient.api.AcknowledgePurchaseResponseListener)) Future acknowledgePurchase(String purchaseToken) async { - assert(purchaseToken != null); return BillingResultWrapper.fromJson((await channel.invokeMapMethod( - 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)', + 'BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)', { 'purchaseToken': purchaseToken, })) ?? @@ -316,26 +324,6 @@ class BillingClient { return result ?? false; } - /// Initiates a flow to confirm the change of price for an item subscribed by the user. - /// - /// When the price of a user subscribed item has changed, launch this flow to take users to - /// a screen with price change information. User can confirm the new price or cancel the flow. - /// - /// The skuDetails needs to have already been fetched in a [querySkuDetails] - /// call. - Future launchPriceChangeConfirmationFlow( - {required String sku}) async { - assert(sku != null); - final Map arguments = { - 'sku': sku, - }; - return BillingResultWrapper.fromJson((await channel.invokeMapMethod( - 'BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)', - arguments)) ?? - {}); - } - /// The method call handler for [channel]. @visibleForTesting Future callHandler(MethodCall call) async { @@ -448,13 +436,13 @@ class BillingResponseConverter implements JsonConverter { int toJson(BillingResponse object) => _$BillingResponseEnumMap[object]!; } -/// Enum representing potential [SkuDetailsWrapper.type]s. +/// Enum representing potential [ProductDetailsWrapper.productType]s. /// /// Wraps -/// [`BillingClient.SkuType`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.SkuType) +/// [`BillingClient.ProductType`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.ProductType) /// See the linked documentation for an explanation of the different constants. @JsonEnum(alwaysCreate: true) -enum SkuType { +enum ProductType { // WARNING: Changes to this class need to be reflected in our generated code. // Run `flutter packages pub run build_runner watch` to rebuild and watch for // further changes. @@ -468,24 +456,24 @@ enum SkuType { subs, } -/// Serializer for [SkuType]. +/// Serializer for [ProductType]. /// /// Use these in `@JsonSerializable()` classes by annotating them with -/// `@SkuTypeConverter()`. -class SkuTypeConverter implements JsonConverter { +/// `@ProductTypeConverter()`. +class ProductTypeConverter implements JsonConverter { /// Default const constructor. - const SkuTypeConverter(); + const ProductTypeConverter(); @override - SkuType fromJson(String? json) { + ProductType fromJson(String? json) { if (json == null) { - return SkuType.inapp; + return ProductType.inapp; } - return $enumDecode(_$SkuTypeEnumMap, json); + return $enumDecode(_$ProductTypeEnumMap, json); } @override - String toJson(SkuType object) => _$SkuTypeEnumMap[object]!; + String toJson(ProductType object) => _$ProductTypeEnumMap[object]!; } /// Enum representing the proration mode. @@ -564,8 +552,9 @@ enum BillingClientFeature { // WARNING: Changes to this class need to be reflected in our generated code. // Run `flutter packages pub run build_runner watch` to rebuild and watch for // further changes. - + // // JsonValues need to match constant values defined in https://developer.android.com/reference/com/android/billingclient/api/BillingClient.FeatureType#summary + /// Purchase/query for in-app items on VR. @JsonValue('inAppItemsOnVr') inAppItemsOnVR, @@ -574,6 +563,10 @@ enum BillingClientFeature { @JsonValue('priceChangeConfirmation') priceChangeConfirmation, + /// Play billing library support for querying and purchasing with ProductDetails. + @JsonValue('fff') + productDetails, + /// Purchase/query for subscriptions. @JsonValue('subscriptions') subscriptions, diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart index 99355a1b91fb..7f636f034347 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart @@ -21,9 +21,9 @@ const _$BillingResponseEnumMap = { BillingResponse.itemNotOwned: 8, }; -const _$SkuTypeEnumMap = { - SkuType.inapp: 'inapp', - SkuType.subs: 'subs', +const _$ProductTypeEnumMap = { + ProductType.inapp: 'inapp', + ProductType.subs: 'subs', }; const _$ProrationModeEnumMap = { @@ -38,6 +38,7 @@ const _$ProrationModeEnumMap = { const _$BillingClientFeatureEnumMap = { BillingClientFeature.inAppItemsOnVR: 'inAppItemsOnVr', BillingClientFeature.priceChangeConfirmation: 'priceChangeConfirmation', + BillingClientFeature.productDetails: 'fff', BillingClientFeature.subscriptions: 'subscriptions', BillingClientFeature.subscriptionsOnVR: 'subscriptionsOnVr', BillingClientFeature.subscriptionsUpdate: 'subscriptionsUpdate', diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_response_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_response_wrapper.dart new file mode 100644 index 000000000000..62887b00d43c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_response_wrapper.dart @@ -0,0 +1,66 @@ +// 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:json_annotation/json_annotation.dart'; + +import '../../billing_client_wrappers.dart'; + +// WARNING: Changes to `@JsonSerializable` classes need to be reflected in the +// below generated file. Run `flutter packages pub run build_runner watch` to +// rebuild and watch for further changes. +part 'billing_response_wrapper.g.dart'; + +/// The error message shown when the map represents billing result is invalid from method channel. +/// +/// This usually indicates a serious underlining code issue in the plugin. +@visibleForTesting +const String kInvalidBillingResultErrorMessage = + 'Invalid billing result map from method channel.'; + +/// Params containing the response code and the debug message from the Play Billing API response. +@JsonSerializable() +@BillingResponseConverter() +@immutable +class BillingResultWrapper implements HasBillingResponse { + /// Constructs the object with [responseCode] and [debugMessage]. + const BillingResultWrapper({required this.responseCode, this.debugMessage}); + + /// Constructs an instance of this from a key value map of data. + /// + /// The map needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory BillingResultWrapper.fromJson(Map? map) { + if (map == null || map.isEmpty) { + return const BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage); + } + return _$BillingResultWrapperFromJson(map); + } + + /// Response code returned in the Play Billing API calls. + @override + final BillingResponse responseCode; + + /// Debug message returned in the Play Billing API calls. + /// + /// Defaults to `null`. + /// This message uses an en-US locale and should not be shown to users. + final String? debugMessage; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + + return other is BillingResultWrapper && + other.responseCode == responseCode && + other.debugMessage == debugMessage; + } + + @override + int get hashCode => Object.hash(responseCode, debugMessage); +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_response_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_response_wrapper.g.dart new file mode 100644 index 000000000000..bff62ae85744 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_response_wrapper.g.dart @@ -0,0 +1,14 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'billing_response_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +BillingResultWrapper _$BillingResultWrapperFromJson(Map json) => + BillingResultWrapper( + responseCode: const BillingResponseConverter() + .fromJson(json['responseCode'] as int?), + debugMessage: json['debugMessage'] as String?, + ); diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/one_time_purchase_offer_details_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/one_time_purchase_offer_details_wrapper.dart new file mode 100644 index 000000000000..f5ceed22ebe9 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/one_time_purchase_offer_details_wrapper.dart @@ -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. + +import 'package:flutter/foundation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +// WARNING: Changes to `@JsonSerializable` classes need to be reflected in the +// below generated file. Run `flutter packages pub run build_runner watch` to +// rebuild and watch for further changes. +part 'one_time_purchase_offer_details_wrapper.g.dart'; + +/// Dart wrapper around [`com.android.billingclient.api.ProductDetails.OneTimePurchaseOfferDetails`](https://developer.android.com/reference/com/android/billingclient/api/ProductDetails.OneTimePurchaseOfferDetails). +/// +/// Represents the offer details to buy a one-time purchase product. +@JsonSerializable() +@immutable +class OneTimePurchaseOfferDetailsWrapper { + /// Creates a [OneTimePurchaseOfferDetailsWrapper]. + @visibleForTesting + const OneTimePurchaseOfferDetailsWrapper({ + required this.formattedPrice, + required this.priceAmountMicros, + required this.priceCurrencyCode, + }); + + /// Factory for creating a [OneTimePurchaseOfferDetailsWrapper] from a [Map] + /// with the offer details. + factory OneTimePurchaseOfferDetailsWrapper.fromJson( + Map map) => + _$OneTimePurchaseOfferDetailsWrapperFromJson(map); + + /// Formatted price for the payment, including its currency sign. + /// + /// For tax exclusive countries, the price doesn't include tax. + @JsonKey(defaultValue: '') + final String formattedPrice; + + /// The price for the payment in micro-units, where 1,000,000 micro-units + /// equal one unit of the currency. + /// + /// For example, if price is "€7.99", price_amount_micros is "7990000". This + /// value represents the localized, rounded price for a particular currency. + @JsonKey(defaultValue: 0) + final int priceAmountMicros; + + /// The ISO 4217 currency code for price. + /// + /// For example, if price is specified in British pounds sterling, currency + /// code is "GBP". + @JsonKey(defaultValue: '') + final String priceCurrencyCode; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + + return other is OneTimePurchaseOfferDetailsWrapper && + other.formattedPrice == formattedPrice && + other.priceAmountMicros == priceAmountMicros && + other.priceCurrencyCode == priceCurrencyCode; + } + + @override + int get hashCode { + return Object.hash( + formattedPrice.hashCode, + priceAmountMicros.hashCode, + priceCurrencyCode.hashCode, + ); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/one_time_purchase_offer_details_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/one_time_purchase_offer_details_wrapper.g.dart new file mode 100644 index 000000000000..19e57e80157b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/one_time_purchase_offer_details_wrapper.g.dart @@ -0,0 +1,15 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'one_time_purchase_offer_details_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +OneTimePurchaseOfferDetailsWrapper _$OneTimePurchaseOfferDetailsWrapperFromJson( + Map json) => + OneTimePurchaseOfferDetailsWrapper( + formattedPrice: json['formattedPrice'] as String? ?? '', + priceAmountMicros: json['priceAmountMicros'] as int? ?? 0, + priceCurrencyCode: json['priceCurrencyCode'] as String? ?? '', + ); diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_details_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_details_wrapper.dart new file mode 100644 index 000000000000..2a7c279b3fca --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_details_wrapper.dart @@ -0,0 +1,190 @@ +// 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/foundation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../billing_client_wrappers.dart'; + +// WARNING: Changes to `@JsonSerializable` classes need to be reflected in the +// below generated file. Run `flutter packages pub run build_runner watch` to +// rebuild and watch for further changes. +part 'product_details_wrapper.g.dart'; + +/// Dart wrapper around [`com.android.billingclient.api.ProductDetails`](https://developer.android.com/reference/com/android/billingclient/api/ProductDetails). +/// +/// Contains the details of an available product in Google Play Billing. +/// Represents the details of a one-time or subscription product. +@JsonSerializable() +@ProductTypeConverter() +@immutable +class ProductDetailsWrapper { + /// Creates a [ProductDetailsWrapper] with the given purchase details. + @visibleForTesting + const ProductDetailsWrapper({ + required this.description, + required this.name, + this.oneTimePurchaseOfferDetails, + required this.productId, + required this.productType, + this.subscriptionOfferDetails, + required this.title, + }); + + /// Factory for creating a [ProductDetailsWrapper] from a [Map] with the + /// product details. + factory ProductDetailsWrapper.fromJson(Map map) => + _$ProductDetailsWrapperFromJson(map); + + /// Textual description of the product. + @JsonKey(defaultValue: '') + final String description; + + /// The name of the product being sold. + /// + /// Similar to [title], but does not include the name of the app which owns + /// the product. Example: 100 Gold Coins. + @JsonKey(defaultValue: '') + final String name; + + /// The offer details of a one-time purchase product. + /// + /// [oneTimePurchaseOfferDetails] is only set for [ProductType.inapp]. Returns + /// null for [ProductType.subs]. + @JsonKey(defaultValue: null) + final OneTimePurchaseOfferDetailsWrapper? oneTimePurchaseOfferDetails; + + /// The product's id. + @JsonKey(defaultValue: '') + final String productId; + + /// The [ProductType] of the product. + @JsonKey(defaultValue: ProductType.subs) + final ProductType productType; + + /// A list containing all available offers to purchase a subscription product. + /// + /// [subscriptionOfferDetails] is only set for [ProductType.subs]. Returns + /// null for [ProductType.inapp]. + @JsonKey(defaultValue: null) + final List? subscriptionOfferDetails; + + /// The title of the product being sold. + /// + /// Similar to [name], but includes the name of the app which owns the + /// product. Example: 100 Gold Coins (Coin selling app). + @JsonKey(defaultValue: '') + final String title; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + + return other is ProductDetailsWrapper && + other.description == description && + other.name == name && + other.oneTimePurchaseOfferDetails == oneTimePurchaseOfferDetails && + other.productId == productId && + other.productType == productType && + listEquals(other.subscriptionOfferDetails, subscriptionOfferDetails) && + other.title == title; + } + + @override + int get hashCode { + return Object.hash( + description.hashCode, + name.hashCode, + oneTimePurchaseOfferDetails.hashCode, + productId.hashCode, + productType.hashCode, + subscriptionOfferDetails.hashCode, + title.hashCode, + ); + } +} + +/// Translation of [`com.android.billingclient.api.ProductDetailsResponseListener`](https://developer.android.com/reference/com/android/billingclient/api/ProductDetailsResponseListener.html). +/// +/// Returned by [BillingClient.queryProductDetails]. +@JsonSerializable() +@immutable +class ProductDetailsResponseWrapper implements HasBillingResponse { + /// Creates a [ProductDetailsResponseWrapper] with the given purchase details. + const ProductDetailsResponseWrapper({ + required this.billingResult, + required this.productDetailsList, + }); + + /// Constructs an instance of this from a key value map of data. + /// + /// The map needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory ProductDetailsResponseWrapper.fromJson(Map map) => + _$ProductDetailsResponseWrapperFromJson(map); + + /// The final result of the [BillingClient.queryProductDetails] call. + final BillingResultWrapper billingResult; + + /// A list of [ProductDetailsWrapper] matching the query to [BillingClient.queryProductDetails]. + @JsonKey(defaultValue: []) + final List productDetailsList; + + @override + BillingResponse get responseCode => billingResult.responseCode; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + + return other is ProductDetailsResponseWrapper && + other.billingResult == billingResult && + other.productDetailsList == productDetailsList; + } + + @override + int get hashCode => Object.hash(billingResult, productDetailsList); +} + +/// Recurrence mode of the pricing phase. +@JsonEnum(alwaysCreate: true) +enum RecurrenceMode { + /// The billing plan payment recurs for a fixed number of billing period set + /// in billingCycleCount. + @JsonValue(2) + finiteRecurring, + + /// The billing plan payment recurs for infinite billing periods unless + /// cancelled. + @JsonValue(1) + infiniteRecurring, + + /// The billing plan payment is a one time charge that does not repeat. + @JsonValue(3) + nonRecurring, +} + +/// Serializer for [RecurrenceMode]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@RecurrenceModeConverter()`. +class RecurrenceModeConverter implements JsonConverter { + /// Default const constructor. + const RecurrenceModeConverter(); + + @override + RecurrenceMode fromJson(int? json) { + if (json == null) { + return RecurrenceMode.nonRecurring; + } + return $enumDecode(_$RecurrenceModeEnumMap, json); + } + + @override + int toJson(RecurrenceMode object) => _$RecurrenceModeEnumMap[object]!; +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_details_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_details_wrapper.g.dart new file mode 100644 index 000000000000..de8079d81571 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_details_wrapper.g.dart @@ -0,0 +1,49 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'product_details_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ProductDetailsWrapper _$ProductDetailsWrapperFromJson(Map json) => + ProductDetailsWrapper( + description: json['description'] as String? ?? '', + name: json['name'] as String? ?? '', + oneTimePurchaseOfferDetails: json['oneTimePurchaseOfferDetails'] == null + ? null + : OneTimePurchaseOfferDetailsWrapper.fromJson( + Map.from( + json['oneTimePurchaseOfferDetails'] as Map)), + productId: json['productId'] as String? ?? '', + productType: json['productType'] == null + ? ProductType.subs + : const ProductTypeConverter() + .fromJson(json['productType'] as String?), + subscriptionOfferDetails: + (json['subscriptionOfferDetails'] as List?) + ?.map((e) => SubscriptionOfferDetailsWrapper.fromJson( + Map.from(e as Map))) + .toList(), + title: json['title'] as String? ?? '', + ); + +ProductDetailsResponseWrapper _$ProductDetailsResponseWrapperFromJson( + Map json) => + ProductDetailsResponseWrapper( + billingResult: + BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + productDetailsList: (json['productDetailsList'] as List?) + ?.map((e) => ProductDetailsWrapper.fromJson( + Map.from(e as Map))) + .toList() ?? + [], + ); + +const _$RecurrenceModeEnumMap = { + RecurrenceMode.finiteRecurring: 2, + RecurrenceMode.infiniteRecurring: 1, + RecurrenceMode.nonRecurring: 3, +}; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_wrapper.dart new file mode 100644 index 000000000000..48cd9ee738ec --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_wrapper.dart @@ -0,0 +1,50 @@ +// 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/foundation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../billing_client_wrappers.dart'; + +// WARNING: Changes to `@JsonSerializable` classes need to be reflected in the +// below generated file. Run `flutter packages pub run build_runner watch` to +// rebuild and watch for further changes. +part 'product_wrapper.g.dart'; + +/// Dart wrapper around [`com.android.billingclient.api.Product`](https://developer.android.com/reference/com/android/billingclient/api/QueryProductDetailsParams.Product). +@JsonSerializable(createToJson: true) +@immutable +class ProductWrapper { + /// Creates a new [ProductWrapper]. + const ProductWrapper({ + required this.productId, + required this.productType, + }); + + /// Creates a JSON representation of this product. + Map toJson() => _$ProductWrapperToJson(this); + + /// The product identifier. + @JsonKey(defaultValue: '') + final String productId; + + /// The product type. + final ProductType productType; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is ProductWrapper && + other.productId == productId && + other.productType == productType; + } + + @override + int get hashCode => Object.hash(productId, productType); +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_wrapper.g.dart new file mode 100644 index 000000000000..c3ba8f4a82ec --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/product_wrapper.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'product_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ProductWrapper _$ProductWrapperFromJson(Map json) => ProductWrapper( + productId: json['productId'] as String? ?? '', + productType: $enumDecode(_$ProductTypeEnumMap, json['productType']), + ); + +Map _$ProductWrapperToJson(ProductWrapper instance) => + { + 'productId': instance.productId, + 'productType': _$ProductTypeEnumMap[instance.productType]!, + }; + +const _$ProductTypeEnumMap = { + ProductType.inapp: 'inapp', + ProductType.subs: 'subs', +}; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart index 633aa732165b..97fde8a8755a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart @@ -6,9 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'billing_client_manager.dart'; -import 'billing_client_wrapper.dart'; -import 'sku_details_wrapper.dart'; +import '../../billing_client_wrappers.dart'; // WARNING: Changes to `@JsonSerializable` classes need to be reflected in the // below generated file. Run `flutter packages pub run build_runner watch` to @@ -34,8 +32,7 @@ class PurchaseWrapper { required this.purchaseTime, required this.purchaseToken, required this.signature, - @Deprecated('Use skus instead') String? sku, - required this.skus, + required this.products, required this.isAutoRenewing, required this.originalJson, this.developerPayload, @@ -43,7 +40,7 @@ class PurchaseWrapper { required this.purchaseState, this.obfuscatedAccountId, this.obfuscatedProfileId, - }) : _sku = sku; + }); /// Factory for creating a [PurchaseWrapper] from a [Map] with the purchase details. factory PurchaseWrapper.fromJson(Map map) => @@ -63,7 +60,7 @@ class PurchaseWrapper { other.purchaseTime == purchaseTime && other.purchaseToken == purchaseToken && other.signature == signature && - other.sku == sku && + listEquals(other.products, products) && other.isAutoRenewing == isAutoRenewing && other.originalJson == originalJson && other.isAcknowledged == isAcknowledged && @@ -77,7 +74,7 @@ class PurchaseWrapper { purchaseTime, purchaseToken, signature, - sku, + products.hashCode, isAutoRenewing, originalJson, isAcknowledged, @@ -96,7 +93,7 @@ class PurchaseWrapper { @JsonKey(defaultValue: 0) final int purchaseTime; - /// A unique ID for a given [SkuDetailsWrapper], user, and purchase. + /// A unique ID for a given [ProductDetailsWrapper], user, and purchase. @JsonKey(defaultValue: '') final String purchaseToken; @@ -105,23 +102,17 @@ class PurchaseWrapper { @JsonKey(defaultValue: '') final String signature; - /// The product ID of this purchase. - @Deprecated('Use skus instead') - @JsonKey(ignore: true) - String get sku => _sku ?? (skus.isNotEmpty ? skus.first : ''); - final String? _sku; - /// The product IDs of this purchase. @JsonKey(defaultValue: []) - final List skus; + final List products; /// True for subscriptions that renew automatically. Does not apply to - /// [SkuType.inapp] products. + /// [ProductType.inapp] products. /// - /// For [SkuType.subs] this means that the subscription is canceled when it is + /// For [ProductType.subs] this means that the subscription is canceled when it is /// false. /// - /// The value is `false` for [SkuType.inapp] products. + /// The value is `false` for [ProductType.inapp] products. final bool isAutoRenewing; /// Details about this purchase, in JSON. @@ -186,11 +177,10 @@ class PurchaseHistoryRecordWrapper { required this.purchaseTime, required this.purchaseToken, required this.signature, - @Deprecated('Use skus instead') String? sku, - required this.skus, + required this.products, required this.originalJson, required this.developerPayload, - }) : _sku = sku; + }); /// Factory for creating a [PurchaseHistoryRecordWrapper] from a [Map] with the record details. factory PurchaseHistoryRecordWrapper.fromJson(Map map) => @@ -200,7 +190,7 @@ class PurchaseHistoryRecordWrapper { @JsonKey(defaultValue: 0) final int purchaseTime; - /// A unique ID for a given [SkuDetailsWrapper], user, and purchase. + /// A unique ID for a given [ProductDetailsWrapper], user, and purchase. @JsonKey(defaultValue: '') final String purchaseToken; @@ -209,16 +199,9 @@ class PurchaseHistoryRecordWrapper { @JsonKey(defaultValue: '') final String signature; - /// The product ID of this purchase. - @Deprecated('Use skus instead') - @JsonKey(ignore: true) - String get sku => _sku ?? (skus.isNotEmpty ? skus.first : ''); - - final String? _sku; - /// The product ID of this purchase. @JsonKey(defaultValue: []) - final List skus; + final List products; /// Details about this purchase, in JSON. /// @@ -246,14 +229,20 @@ class PurchaseHistoryRecordWrapper { other.purchaseTime == purchaseTime && other.purchaseToken == purchaseToken && other.signature == signature && - other.sku == sku && + listEquals(other.products, products) && other.originalJson == originalJson && other.developerPayload == developerPayload; } @override - int get hashCode => Object.hash(purchaseTime, purchaseToken, signature, sku, - originalJson, developerPayload); + int get hashCode => Object.hash( + purchaseTime, + purchaseToken, + signature, + products.hashCode, + originalJson, + developerPayload, + ); } /// A data struct representing the result of a transaction. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart index ad2a909fbfdc..0270d610eb68 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart @@ -12,9 +12,10 @@ PurchaseWrapper _$PurchaseWrapperFromJson(Map json) => PurchaseWrapper( purchaseTime: json['purchaseTime'] as int? ?? 0, purchaseToken: json['purchaseToken'] as String? ?? '', signature: json['signature'] as String? ?? '', - skus: - (json['skus'] as List?)?.map((e) => e as String).toList() ?? - [], + products: (json['products'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], isAutoRenewing: json['isAutoRenewing'] as bool, originalJson: json['originalJson'] as String? ?? '', developerPayload: json['developerPayload'] as String?, @@ -30,9 +31,10 @@ PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) => purchaseTime: json['purchaseTime'] as int? ?? 0, purchaseToken: json['purchaseToken'] as String? ?? '', signature: json['signature'] as String? ?? '', - skus: - (json['skus'] as List?)?.map((e) => e as String).toList() ?? - [], + products: (json['products'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], originalJson: json['originalJson'] as String? ?? '', developerPayload: json['developerPayload'] as String?, ); diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart deleted file mode 100644 index 2689cf37eac4..000000000000 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart +++ /dev/null @@ -1,268 +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/foundation.dart'; -import 'package:json_annotation/json_annotation.dart'; - -import 'billing_client_manager.dart'; -import 'billing_client_wrapper.dart'; - -// WARNING: Changes to `@JsonSerializable` classes need to be reflected in the -// below generated file. Run `flutter packages pub run build_runner watch` to -// rebuild and watch for further changes. -part 'sku_details_wrapper.g.dart'; - -/// The error message shown when the map represents billing result is invalid from method channel. -/// -/// This usually indicates a series underlining code issue in the plugin. -@visibleForTesting -const String kInvalidBillingResultErrorMessage = - 'Invalid billing result map from method channel.'; - -/// Dart wrapper around [`com.android.billingclient.api.SkuDetails`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails). -/// -/// Contains the details of an available product in Google Play Billing. -@JsonSerializable() -@SkuTypeConverter() -@immutable -class SkuDetailsWrapper { - /// Creates a [SkuDetailsWrapper] with the given purchase details. - @visibleForTesting - const SkuDetailsWrapper({ - required this.description, - required this.freeTrialPeriod, - required this.introductoryPrice, - @Deprecated('Use `introductoryPriceAmountMicros` parameter instead') - String introductoryPriceMicros = '', - this.introductoryPriceAmountMicros = 0, - required this.introductoryPriceCycles, - required this.introductoryPricePeriod, - required this.price, - required this.priceAmountMicros, - required this.priceCurrencyCode, - required this.priceCurrencySymbol, - required this.sku, - required this.subscriptionPeriod, - required this.title, - required this.type, - required this.originalPrice, - required this.originalPriceAmountMicros, - }) : _introductoryPriceMicros = introductoryPriceMicros; - - /// Constructs an instance of this from a key value map of data. - /// - /// The map needs to have named string keys with values matching the names and - /// types of all of the members on this class. - @visibleForTesting - factory SkuDetailsWrapper.fromJson(Map map) => - _$SkuDetailsWrapperFromJson(map); - - final String _introductoryPriceMicros; - - /// Textual description of the product. - @JsonKey(defaultValue: '') - final String description; - - /// Trial period in ISO 8601 format. - @JsonKey(defaultValue: '') - final String freeTrialPeriod; - - /// Introductory price, only applies to [SkuType.subs]. Formatted ("$0.99"). - @JsonKey(defaultValue: '') - final String introductoryPrice; - - /// [introductoryPrice] in micro-units 990000. - /// - /// Returns 0 if the SKU is not a subscription or doesn't have an introductory - /// period. - final int introductoryPriceAmountMicros; - - /// String representation of [introductoryPrice] in micro-units 990000 - @Deprecated('Use `introductoryPriceAmountMicros` instead.') - @JsonKey(ignore: true) - String get introductoryPriceMicros => _introductoryPriceMicros.isEmpty - ? introductoryPriceAmountMicros.toString() - : _introductoryPriceMicros; - - /// The number of subscription billing periods for which the user will be given the introductory price, such as 3. - /// Returns 0 if the SKU is not a subscription or doesn't have an introductory period. - @JsonKey(defaultValue: 0) - final int introductoryPriceCycles; - - /// The billing period of [introductoryPrice], in ISO 8601 format. - @JsonKey(defaultValue: '') - final String introductoryPricePeriod; - - /// Formatted with currency symbol ("$0.99"). - @JsonKey(defaultValue: '') - final String price; - - /// [price] in micro-units ("990000"). - @JsonKey(defaultValue: 0) - final int priceAmountMicros; - - /// [price] ISO 4217 currency code. - @JsonKey(defaultValue: '') - final String priceCurrencyCode; - - /// [price] localized currency symbol - /// For example, for the US Dollar, the symbol is "$" if the locale - /// is the US, while for other locales it may be "US$". - @JsonKey(defaultValue: '') - final String priceCurrencySymbol; - - /// The product ID in Google Play Console. - @JsonKey(defaultValue: '') - final String sku; - - /// Applies to [SkuType.subs], formatted in ISO 8601. - @JsonKey(defaultValue: '') - final String subscriptionPeriod; - - /// The product's title. - @JsonKey(defaultValue: '') - final String title; - - /// The [SkuType] of the product. - final SkuType type; - - /// The original price that the user purchased this product for. - @JsonKey(defaultValue: '') - final String originalPrice; - - /// [originalPrice] in micro-units ("990000"). - @JsonKey(defaultValue: 0) - final int originalPriceAmountMicros; - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) { - return false; - } - - return other is SkuDetailsWrapper && - other.description == description && - other.freeTrialPeriod == freeTrialPeriod && - other.introductoryPrice == introductoryPrice && - other.introductoryPriceAmountMicros == introductoryPriceAmountMicros && - other.introductoryPriceCycles == introductoryPriceCycles && - other.introductoryPricePeriod == introductoryPricePeriod && - other.price == price && - other.priceAmountMicros == priceAmountMicros && - other.sku == sku && - other.subscriptionPeriod == subscriptionPeriod && - other.title == title && - other.type == type && - other.originalPrice == originalPrice && - other.originalPriceAmountMicros == originalPriceAmountMicros; - } - - @override - int get hashCode { - return Object.hash( - description.hashCode, - freeTrialPeriod.hashCode, - introductoryPrice.hashCode, - introductoryPriceAmountMicros.hashCode, - introductoryPriceCycles.hashCode, - introductoryPricePeriod.hashCode, - price.hashCode, - priceAmountMicros.hashCode, - sku.hashCode, - subscriptionPeriod.hashCode, - title.hashCode, - type.hashCode, - originalPrice, - originalPriceAmountMicros); - } -} - -/// Translation of [`com.android.billingclient.api.SkuDetailsResponseListener`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetailsResponseListener.html). -/// -/// Returned by [BillingClient.querySkuDetails]. -@JsonSerializable() -@immutable -class SkuDetailsResponseWrapper implements HasBillingResponse { - /// Creates a [SkuDetailsResponseWrapper] with the given purchase details. - @visibleForTesting - const SkuDetailsResponseWrapper( - {required this.billingResult, required this.skuDetailsList}); - - /// Constructs an instance of this from a key value map of data. - /// - /// The map needs to have named string keys with values matching the names and - /// types of all of the members on this class. - factory SkuDetailsResponseWrapper.fromJson(Map map) => - _$SkuDetailsResponseWrapperFromJson(map); - - /// The final result of the [BillingClient.querySkuDetails] call. - final BillingResultWrapper billingResult; - - /// A list of [SkuDetailsWrapper] matching the query to [BillingClient.querySkuDetails]. - @JsonKey(defaultValue: []) - final List skuDetailsList; - - @override - BillingResponse get responseCode => billingResult.responseCode; - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) { - return false; - } - - return other is SkuDetailsResponseWrapper && - other.billingResult == billingResult && - other.skuDetailsList == skuDetailsList; - } - - @override - int get hashCode => Object.hash(billingResult, skuDetailsList); -} - -/// Params containing the response code and the debug message from the Play Billing API response. -@JsonSerializable() -@BillingResponseConverter() -@immutable -class BillingResultWrapper implements HasBillingResponse { - /// Constructs the object with [responseCode] and [debugMessage]. - const BillingResultWrapper({required this.responseCode, this.debugMessage}); - - /// Constructs an instance of this from a key value map of data. - /// - /// The map needs to have named string keys with values matching the names and - /// types of all of the members on this class. - factory BillingResultWrapper.fromJson(Map? map) { - if (map == null || map.isEmpty) { - return const BillingResultWrapper( - responseCode: BillingResponse.error, - debugMessage: kInvalidBillingResultErrorMessage); - } - return _$BillingResultWrapperFromJson(map); - } - - /// Response code returned in the Play Billing API calls. - @override - final BillingResponse responseCode; - - /// Debug message returned in the Play Billing API calls. - /// - /// Defaults to `null`. - /// This message uses an en-US locale and should not be shown to users. - final String? debugMessage; - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) { - return false; - } - - return other is BillingResultWrapper && - other.responseCode == responseCode && - other.debugMessage == debugMessage; - } - - @override - int get hashCode => Object.hash(responseCode, debugMessage); -} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart deleted file mode 100644 index 05eb6bed0035..000000000000 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart +++ /dev/null @@ -1,47 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'sku_details_wrapper.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) => SkuDetailsWrapper( - description: json['description'] as String? ?? '', - freeTrialPeriod: json['freeTrialPeriod'] as String? ?? '', - introductoryPrice: json['introductoryPrice'] as String? ?? '', - introductoryPriceAmountMicros: - json['introductoryPriceAmountMicros'] as int? ?? 0, - introductoryPriceCycles: json['introductoryPriceCycles'] as int? ?? 0, - introductoryPricePeriod: json['introductoryPricePeriod'] as String? ?? '', - price: json['price'] as String? ?? '', - priceAmountMicros: json['priceAmountMicros'] as int? ?? 0, - priceCurrencyCode: json['priceCurrencyCode'] as String? ?? '', - priceCurrencySymbol: json['priceCurrencySymbol'] as String? ?? '', - sku: json['sku'] as String? ?? '', - subscriptionPeriod: json['subscriptionPeriod'] as String? ?? '', - title: json['title'] as String? ?? '', - type: const SkuTypeConverter().fromJson(json['type'] as String?), - originalPrice: json['originalPrice'] as String? ?? '', - originalPriceAmountMicros: json['originalPriceAmountMicros'] as int? ?? 0, - ); - -SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) => - SkuDetailsResponseWrapper( - billingResult: - BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - skuDetailsList: (json['skuDetailsList'] as List?) - ?.map((e) => SkuDetailsWrapper.fromJson( - Map.from(e as Map))) - .toList() ?? - [], - ); - -BillingResultWrapper _$BillingResultWrapperFromJson(Map json) => - BillingResultWrapper( - responseCode: const BillingResponseConverter() - .fromJson(json['responseCode'] as int?), - debugMessage: json['debugMessage'] as String?, - ); diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/subscription_offer_details_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/subscription_offer_details_wrapper.dart new file mode 100644 index 000000000000..aa5688eb60f3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/subscription_offer_details_wrapper.dart @@ -0,0 +1,158 @@ +// 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/foundation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'billing_client_wrapper.dart'; +import 'product_details_wrapper.dart'; + +// WARNING: Changes to `@JsonSerializable` classes need to be reflected in the +// below generated file. Run `flutter packages pub run build_runner watch` to +// rebuild and watch for further changes. +part 'subscription_offer_details_wrapper.g.dart'; + +/// Dart wrapper around [`com.android.billingclient.api.ProductDetails.SubscriptionOfferDetails`](https://developer.android.com/reference/com/android/billingclient/api/ProductDetails.SubscriptionOfferDetails). +/// +/// Represents the available purchase plans to buy a subscription product. +@JsonSerializable() +@immutable +class SubscriptionOfferDetailsWrapper { + /// Creates a [SubscriptionOfferDetailsWrapper]. + @visibleForTesting + const SubscriptionOfferDetailsWrapper({ + required this.basePlanId, + this.offerId, + required this.offerTags, + required this.offerIdToken, + required this.pricingPhases, + }); + + /// Factory for creating a [SubscriptionOfferDetailsWrapper] from a [Map] + /// with the offer details. + factory SubscriptionOfferDetailsWrapper.fromJson(Map map) => + _$SubscriptionOfferDetailsWrapperFromJson(map); + + /// The base plan id associated with the subscription product. + @JsonKey(defaultValue: '') + final String basePlanId; + + /// The offer id associated with the subscription product. + /// + /// This field is only set for a discounted offer. Returns null for a regular + /// base plan. + @JsonKey(defaultValue: null) + final String? offerId; + + /// The offer tags associated with this Subscription Offer. + @JsonKey(defaultValue: []) + final List offerTags; + + /// The offer token required to pass in [BillingClient.launchBillingFlow] to + /// purchase the subscription product with these [pricingPhases]. + @JsonKey(defaultValue: '') + final String offerIdToken; + + /// The pricing phases for the subscription product. + @JsonKey(defaultValue: []) + final List pricingPhases; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + + return other is SubscriptionOfferDetailsWrapper && + other.basePlanId == basePlanId && + other.offerId == offerId && + listEquals(other.offerTags, offerTags) && + other.offerIdToken == offerIdToken && + listEquals(other.pricingPhases, pricingPhases); + } + + @override + int get hashCode { + return Object.hash( + basePlanId.hashCode, + offerId.hashCode, + offerTags.hashCode, + offerIdToken.hashCode, + pricingPhases.hashCode, + ); + } +} + +/// Represents a pricing phase, describing how a user pays at a point in time. +@JsonSerializable() +@RecurrenceModeConverter() +@immutable +class PricingPhaseWrapper { + /// Creates a new [PricingPhaseWrapper] from the supplied info. + @visibleForTesting + const PricingPhaseWrapper({ + required this.billingCycleCount, + required this.billingPeriod, + required this.formattedPrice, + required this.priceAmountMicros, + required this.priceCurrencyCode, + required this.recurrenceMode, + }); + + /// Factory for creating a [PricingPhaseWrapper] from a [Map] with the phase details. + factory PricingPhaseWrapper.fromJson(Map map) => + _$PricingPhaseWrapperFromJson(map); + + /// Represents a pricing phase, describing how a user pays at a point in time. + @JsonKey(defaultValue: 0) + final int billingCycleCount; + + /// Billing period for which the given price applies, specified in ISO 8601 + /// format. + @JsonKey(defaultValue: '') + final String billingPeriod; + + /// Returns formatted price for the payment cycle, including its currency + /// sign. + @JsonKey(defaultValue: '') + final String formattedPrice; + + /// Returns the price for the payment cycle in micro-units, where 1,000,000 + /// micro-units equal one unit of the currency. + @JsonKey(defaultValue: 0) + final int priceAmountMicros; + + /// Returns ISO 4217 currency code for price. + @JsonKey(defaultValue: '') + final String priceCurrencyCode; + + /// Returns [RecurrenceMode] for the pricing phase. + @JsonKey(defaultValue: RecurrenceMode.nonRecurring) + final RecurrenceMode recurrenceMode; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + + return other is PricingPhaseWrapper && + other.billingCycleCount == billingCycleCount && + other.billingPeriod == billingPeriod && + other.formattedPrice == formattedPrice && + other.priceAmountMicros == priceAmountMicros && + other.priceCurrencyCode == priceCurrencyCode && + other.recurrenceMode == recurrenceMode; + } + + @override + int get hashCode => Object.hash( + billingCycleCount, + billingPeriod, + formattedPrice, + priceAmountMicros, + priceCurrencyCode, + recurrenceMode, + ); +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/subscription_offer_details_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/subscription_offer_details_wrapper.g.dart new file mode 100644 index 000000000000..eca645340fe5 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/subscription_offer_details_wrapper.g.dart @@ -0,0 +1,37 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'subscription_offer_details_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SubscriptionOfferDetailsWrapper _$SubscriptionOfferDetailsWrapperFromJson( + Map json) => + SubscriptionOfferDetailsWrapper( + basePlanId: json['basePlanId'] as String? ?? '', + offerId: json['offerId'] as String?, + offerTags: (json['offerTags'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], + offerIdToken: json['offerIdToken'] as String? ?? '', + pricingPhases: (json['pricingPhases'] as List?) + ?.map((e) => PricingPhaseWrapper.fromJson( + Map.from(e as Map))) + .toList() ?? + [], + ); + +PricingPhaseWrapper _$PricingPhaseWrapperFromJson(Map json) => + PricingPhaseWrapper( + billingCycleCount: json['billingCycleCount'] as int? ?? 0, + billingPeriod: json['billingPeriod'] as String? ?? '', + formattedPrice: json['formattedPrice'] as String? ?? '', + priceAmountMicros: json['priceAmountMicros'] as int? ?? 0, + priceCurrencyCode: json['priceCurrencyCode'] as String? ?? '', + recurrenceMode: json['recurrenceMode'] == null + ? RecurrenceMode.nonRecurring + : const RecurrenceModeConverter() + .fromJson(json['recurrenceMode'] as int?), + ); diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart index b605c2f611c6..2e4ed5b0f927 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart @@ -66,44 +66,53 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { .runWithClientNonRetryable((BillingClient client) => client.isReady()); } + /// Performs a network query for the details of products available. @override Future queryProductDetails( - Set identifiers) async { - List responses; + Set identifiers, + ) async { + List? productResponses; PlatformException? exception; - Future querySkuDetails(SkuType type) { - return billingClientManager.runWithClient( - (BillingClient client) => client.querySkuDetails( - skuType: type, - skusList: identifiers.toList(), - ), - ); - } - try { - responses = await Future.wait(>[ - querySkuDetails(SkuType.inapp), - querySkuDetails(SkuType.subs), - ]); + productResponses = await Future.wait( + >[ + billingClientManager.runWithClient( + (BillingClient client) => client.queryProductDetails( + productList: identifiers + .map((String productId) => ProductWrapper( + productId: productId, productType: ProductType.inapp)) + .toList(), + ), + ), + billingClientManager.runWithClient( + (BillingClient client) => client.queryProductDetails( + productList: identifiers + .map((String productId) => ProductWrapper( + productId: productId, productType: ProductType.subs)) + .toList(), + ), + ), + ], + ); } on PlatformException catch (e) { exception = e; - // ignore: invalid_use_of_visible_for_testing_member - final SkuDetailsResponseWrapper response = SkuDetailsResponseWrapper( - billingResult: BillingResultWrapper( - responseCode: BillingResponse.error, - debugMessage: e.code, - ), - skuDetailsList: const [], - ); - // Error response for both queries should be the same, so we can reuse it. - responses = [response, response]; + productResponses = [ + ProductDetailsResponseWrapper( + billingResult: BillingResultWrapper( + responseCode: BillingResponse.error, debugMessage: e.code), + productDetailsList: const []), + ProductDetailsResponseWrapper( + billingResult: BillingResultWrapper( + responseCode: BillingResponse.error, debugMessage: e.code), + productDetailsList: const []) + ]; } final List productDetailsList = - responses.expand((SkuDetailsResponseWrapper response) { - return response.skuDetailsList; - }).map((SkuDetailsWrapper skuDetailWrapper) { - return GooglePlayProductDetails.fromSkuDetails(skuDetailWrapper); + productResponses.expand((ProductDetailsResponseWrapper response) { + return response.productDetailsList; + }).expand((ProductDetailsWrapper productDetailWrapper) { + return GooglePlayProductDetails.fromProductDetails(productDetailWrapper); }).toList(); final Set successIDS = productDetailsList @@ -131,16 +140,22 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { changeSubscriptionParam = purchaseParam.changeSubscriptionParam; } + String? offerToken; + if (purchaseParam.productDetails is GooglePlayProductDetails) { + offerToken = + (purchaseParam.productDetails as GooglePlayProductDetails).offerToken; + } + final BillingResultWrapper billingResultWrapper = await billingClientManager.runWithClient( (BillingClient client) => client.launchBillingFlow( - sku: purchaseParam.productDetails.id, - accountId: purchaseParam.applicationUserName, - oldSku: changeSubscriptionParam?.oldPurchaseDetails.productID, - purchaseToken: changeSubscriptionParam - ?.oldPurchaseDetails.verificationData.serverVerificationData, - prorationMode: changeSubscriptionParam?.prorationMode, - ), + product: purchaseParam.productDetails.id, + offerToken: offerToken, + accountId: purchaseParam.applicationUserName, + oldProduct: changeSubscriptionParam?.oldPurchaseDetails.productID, + purchaseToken: changeSubscriptionParam + ?.oldPurchaseDetails.verificationData.serverVerificationData, + prorationMode: changeSubscriptionParam?.prorationMode), ); return billingResultWrapper.responseCode == BillingResponse.ok; } @@ -169,11 +184,6 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { return const BillingResultWrapper(responseCode: BillingResponse.ok); } - if (googlePurchase.verificationData == null) { - throw ArgumentError( - 'completePurchase unsuccessful. The `purchase.verificationData` is not valid'); - } - return billingClientManager.runWithClient( (BillingClient client) => client.acknowledgePurchase( purchase.verificationData.serverVerificationData), @@ -188,10 +198,10 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { responses = await Future.wait(>[ billingClientManager.runWithClient( - (BillingClient client) => client.queryPurchases(SkuType.inapp), + (BillingClient client) => client.queryPurchases(ProductType.inapp), ), billingClientManager.runWithClient( - (BillingClient client) => client.queryPurchases(SkuType.subs), + (BillingClient client) => client.queryPurchases(ProductType.subs), ), ]); @@ -205,17 +215,13 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { final String errorMessage = errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; - final List pastPurchases = - responses.expand((PurchasesResultWrapper response) { - return response.purchasesList; - }).map((PurchaseWrapper purchaseWrapper) { - final GooglePlayPurchaseDetails purchaseDetails = - GooglePlayPurchaseDetails.fromPurchase(purchaseWrapper); - - purchaseDetails.status = PurchaseStatus.restored; - - return purchaseDetails; - }).toList(); + final List pastPurchases = responses + .expand((PurchasesResultWrapper response) => response.purchasesList) + .expand((PurchaseWrapper purchaseWrapper) => + GooglePlayPurchaseDetails.fromPurchase(purchaseWrapper)) + .map((GooglePlayPurchaseDetails details) => + details..status = PurchaseStatus.restored) + .toList(); if (errorMessage.isNotEmpty) { throw InAppPurchaseException( @@ -265,14 +271,15 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { details: resultWrapper.billingResult.debugMessage, ); } - final List> purchases = - resultWrapper.purchasesList.map((PurchaseWrapper purchase) { - final GooglePlayPurchaseDetails googlePlayPurchaseDetails = - GooglePlayPurchaseDetails.fromPurchase(purchase)..error = error; + final List> purchases = resultWrapper.purchasesList + .expand((PurchaseWrapper purchase) => + GooglePlayPurchaseDetails.fromPurchase(purchase)) + .map((GooglePlayPurchaseDetails purchaseDetails) { + purchaseDetails.error = error; if (resultWrapper.responseCode == BillingResponse.userCanceled) { - googlePlayPurchaseDetails.status = PurchaseStatus.canceled; + purchaseDetails.status = PurchaseStatus.canceled; } - return _maybeAutoConsumePurchase(googlePlayPurchaseDetails); + return _maybeAutoConsumePurchase(purchaseDetails); }).toList(); if (purchases.isNotEmpty) { return Future.wait(purchases); diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart index 67e21dfad8f8..eb58d1536c20 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart @@ -50,10 +50,6 @@ class InAppPurchaseAndroidPlatformAddition /// delivered. The user won't be able to buy the same product again until the /// purchase of the product is consumed. Future consumePurchase(PurchaseDetails purchase) { - if (purchase.verificationData == null) { - throw ArgumentError( - 'consumePurchase unsuccessful. The `purchase.verificationData` is not valid'); - } return _billingClientManager.runWithClient( (BillingClient client) => client.consumeAsync(purchase.verificationData.serverVerificationData), @@ -78,13 +74,14 @@ class InAppPurchaseAndroidPlatformAddition {String? applicationUserName}) async { List responses; PlatformException? exception; + try { responses = await Future.wait(>[ _billingClientManager.runWithClient( - (BillingClient client) => client.queryPurchases(SkuType.inapp), + (BillingClient client) => client.queryPurchases(ProductType.inapp), ), _billingClientManager.runWithClient( - (BillingClient client) => client.queryPurchases(SkuType.subs), + (BillingClient client) => client.queryPurchases(ProductType.subs), ), ]); } on PlatformException catch (e) { @@ -119,12 +116,11 @@ class InAppPurchaseAndroidPlatformAddition final String errorMessage = errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; - final List pastPurchases = - responses.expand((PurchasesResultWrapper response) { - return response.purchasesList; - }).map((PurchaseWrapper purchaseWrapper) { - return GooglePlayPurchaseDetails.fromPurchase(purchaseWrapper); - }).toList(); + final List pastPurchases = responses + .expand((PurchasesResultWrapper response) => response.purchasesList) + .expand((PurchaseWrapper purchaseWrapper) => + GooglePlayPurchaseDetails.fromPurchase(purchaseWrapper)) + .toList(); IAPError? error; if (exception != null) { @@ -151,19 +147,4 @@ class InAppPurchaseAndroidPlatformAddition (BillingClient client) => client.isFeatureSupported(feature), ); } - - /// Initiates a flow to confirm the change of price for an item subscribed by the user. - /// - /// When the price of a user subscribed item has changed, launch this flow to take users to - /// a screen with price change information. User can confirm the new price or cancel the flow. - /// - /// The skuDetails needs to have already been fetched in a - /// [InAppPurchaseAndroidPlatform.queryProductDetails] call. - Future launchPriceChangeConfirmationFlow( - {required String sku}) { - return _billingClientManager.runWithClient( - (BillingClient client) => - client.launchPriceChangeConfirmationFlow(sku: sku), - ); - } } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_product_details.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_product_details.dart index 66dbf61236cb..0d3beaea513a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_product_details.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_product_details.dart @@ -11,35 +11,151 @@ import '../../billing_client_wrappers.dart'; class GooglePlayProductDetails extends ProductDetails { /// Creates a new Google Play specific product details object with the /// provided details. - GooglePlayProductDetails({ + GooglePlayProductDetails._({ required super.id, required super.title, required super.description, required super.price, required super.rawPrice, required super.currencyCode, - required this.skuDetails, + required this.productDetails, required super.currencySymbol, + this.subscriptionIndex, }); - /// Generate a [GooglePlayProductDetails] object based on an Android - /// [SkuDetailsWrapper] object. - factory GooglePlayProductDetails.fromSkuDetails( - SkuDetailsWrapper skuDetails, + /// Generates a [GooglePlayProductDetails] object based on an Android + /// [ProductDetailsWrapper] object for an in-app product. + factory GooglePlayProductDetails._fromOneTimePurchaseProductDetails( + ProductDetailsWrapper productDetails, ) { - return GooglePlayProductDetails( - id: skuDetails.sku, - title: skuDetails.title, - description: skuDetails.description, - price: skuDetails.price, - rawPrice: skuDetails.priceAmountMicros / 1000000.0, - currencyCode: skuDetails.priceCurrencyCode, - currencySymbol: skuDetails.priceCurrencySymbol, - skuDetails: skuDetails, + assert(productDetails.productType == ProductType.inapp); + assert(productDetails.oneTimePurchaseOfferDetails != null); + + final OneTimePurchaseOfferDetailsWrapper oneTimePurchaseOfferDetails = + productDetails.oneTimePurchaseOfferDetails!; + + final String formattedPrice = oneTimePurchaseOfferDetails.formattedPrice; + final double rawPrice = + oneTimePurchaseOfferDetails.priceAmountMicros / 1000000.0; + final String currencyCode = oneTimePurchaseOfferDetails.priceCurrencyCode; + final String? currencySymbol = _extractCurrencySymbol(formattedPrice); + + return GooglePlayProductDetails._( + id: productDetails.productId, + title: productDetails.title, + description: productDetails.description, + price: formattedPrice, + rawPrice: rawPrice, + currencyCode: currencyCode, + currencySymbol: currencySymbol ?? currencyCode, + productDetails: productDetails, + ); + } + + /// Generates a [GooglePlayProductDetails] object based on an Android + /// [ProductDetailsWrapper] object for a subscription product. + /// + /// Subscriptions can consist of multiple base plans, and base plans in turn + /// can consist of multiple offers. [subscriptionIndex] points to the index of + /// [productDetails.subscriptionOfferDetails] for which the + /// [GooglePlayProductDetails] is constructed. + factory GooglePlayProductDetails._fromSubscription( + ProductDetailsWrapper productDetails, + int subscriptionIndex, + ) { + assert(productDetails.productType == ProductType.subs); + assert(productDetails.subscriptionOfferDetails != null); + assert(subscriptionIndex < productDetails.subscriptionOfferDetails!.length); + + final SubscriptionOfferDetailsWrapper subscriptionOfferDetails = + productDetails.subscriptionOfferDetails![subscriptionIndex]; + + final PricingPhaseWrapper firstPricingPhase = + subscriptionOfferDetails.pricingPhases.first; + final String formattedPrice = firstPricingPhase.formattedPrice; + final double rawPrice = firstPricingPhase.priceAmountMicros / 1000000.0; + final String currencyCode = firstPricingPhase.priceCurrencyCode; + final String? currencySymbol = _extractCurrencySymbol(formattedPrice); + + return GooglePlayProductDetails._( + id: productDetails.productId, + title: productDetails.title, + description: productDetails.description, + price: formattedPrice, + rawPrice: rawPrice, + currencyCode: currencyCode, + currencySymbol: currencySymbol ?? currencyCode, + productDetails: productDetails, + subscriptionIndex: subscriptionIndex, ); } - /// Points back to the [SkuDetailsWrapper] object that was used to generate - /// this [GooglePlayProductDetails] object. - final SkuDetailsWrapper skuDetails; + /// Generates a list of [GooglePlayProductDetails] based on an Android + /// [ProductDetailsWrapper] object. + /// + /// If [productDetails] is of type [ProductType.inapp], a single + /// [GooglePlayProductDetails] will be constructed. + /// If [productDetails] is of type [ProductType.subs], a list is returned + /// where every element corresponds to a base plan or its offer in + /// [productDetails.subscriptionOfferDetails]. + static List fromProductDetails( + ProductDetailsWrapper productDetails, + ) { + if (productDetails.productType == ProductType.inapp) { + return [ + GooglePlayProductDetails._fromOneTimePurchaseProductDetails( + productDetails), + ]; + } else { + final List productDetailList = + []; + for (int subscriptionIndex = 0; + subscriptionIndex < productDetails.subscriptionOfferDetails!.length; + subscriptionIndex++) { + productDetailList.add(GooglePlayProductDetails._fromSubscription( + productDetails, + subscriptionIndex, + )); + } + + return productDetailList; + } + } + + /// Extracts the currency symbol from [formattedPrice]. + /// + /// Note that a currency symbol might consist of more than a single character. + /// + /// Just in case, we assume currency symbols can appear at the start or the + /// end of [formattedPrice]. + /// + /// The regex captures the characters from the start/end of the [String] + /// until the first/last digit or space. + static String? _extractCurrencySymbol(String formattedPrice) { + return RegExp(r'^[^\d ]*|[^\d ]*$').firstMatch(formattedPrice)?.group(0); + } + + /// Points back to the [ProductDetailsWrapper] object that was used to + /// generate this [GooglePlayProductDetails] object. + final ProductDetailsWrapper productDetails; + + /// The index pointing to the [SubscriptionOfferDetailsWrapper] this + /// [GooglePlayProductDetails] object was contructed for, or `null` if it was + /// not a subscription. + /// + /// The original subscription can be accessed using this index: + /// + /// ```dart + /// SubscriptionOfferDetailWrapper subscription = productDetail + /// .subscriptionOfferDetails[subscriptionIndex]; + /// ``` + final int? subscriptionIndex; + + /// The offerToken of the subscription this [GooglePlayProductDetails] + /// object was contructed for, or `null` if it was not a subscription. + String? get offerToken => subscriptionIndex != null && + productDetails.subscriptionOfferDetails != null + ? productDetails + .subscriptionOfferDetails![subscriptionIndex!].offerIdToken + : null; } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart index 9bf3fc5563bb..f2596d61326d 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart @@ -22,30 +22,36 @@ class GooglePlayPurchaseDetails extends PurchaseDetails { pendingCompletePurchase = !billingClientPurchase.isAcknowledged; } - /// Generate a [PurchaseDetails] object based on an Android [Purchase] object. - factory GooglePlayPurchaseDetails.fromPurchase(PurchaseWrapper purchase) { - final GooglePlayPurchaseDetails purchaseDetails = GooglePlayPurchaseDetails( - purchaseID: purchase.orderId, - productID: purchase.sku, - verificationData: PurchaseVerificationData( - localVerificationData: purchase.originalJson, - serverVerificationData: purchase.purchaseToken, - source: kIAPSource), - transactionDate: purchase.purchaseTime.toString(), - billingClientPurchase: purchase, - status: const PurchaseStateConverter() - .toPurchaseStatus(purchase.purchaseState), - ); - - if (purchaseDetails.status == PurchaseStatus.error) { - purchaseDetails.error = IAPError( - source: kIAPSource, - code: kPurchaseErrorCode, - message: '', + /// Generates a [List] of [PurchaseDetails] based on an Android [Purchase] object. + /// + /// The list contains one entry per product. + static List fromPurchase( + PurchaseWrapper purchase) { + return purchase.products.map((String productId) { + final GooglePlayPurchaseDetails purchaseDetails = + GooglePlayPurchaseDetails( + purchaseID: purchase.orderId, + productID: productId, + verificationData: PurchaseVerificationData( + localVerificationData: purchase.originalJson, + serverVerificationData: purchase.purchaseToken, + source: kIAPSource), + transactionDate: purchase.purchaseTime.toString(), + billingClientPurchase: purchase, + status: const PurchaseStateConverter() + .toPurchaseStatus(purchase.purchaseState), ); - } - return purchaseDetails; + if (purchaseDetails.status == PurchaseStatus.error) { + purchaseDetails.error = IAPError( + source: kIAPSource, + code: kPurchaseErrorCode, + message: '', + ); + } + + return purchaseDetails; + }).toList(); } /// Points back to the [PurchaseWrapper] which was used to generate this diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml index f1aa0e10a7ee..a29be3d68bca 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/packages/tree/main/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.2.5+5 +version: 0.3.0+2 environment: sdk: ">=2.18.0 <4.0.0" @@ -21,7 +21,7 @@ dependencies: flutter: sdk: flutter in_app_purchase_platform_interface: ^1.3.0 - json_annotation: ^4.6.0 + json_annotation: ^4.8.0 dev_dependencies: build_runner: ^2.0.0 diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart index 1644885d78da..97f4640c7eee 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart @@ -51,25 +51,29 @@ void main() { expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1)); }); - test('waits for connection before executing the operations', () { - bool called1 = false; - bool called2 = false; + test('waits for connection before executing the operations', () async { + final Completer calledCompleter1 = Completer(); + final Completer calledCompleter2 = Completer(); manager.runWithClient((BillingClient _) async { - called1 = true; + calledCompleter1.complete(); return const BillingResultWrapper(responseCode: BillingResponse.ok); }); manager.runWithClientNonRetryable( - (BillingClient _) async => called2 = true, + (BillingClient _) async => calledCompleter2.complete(), ); - expect(called1, equals(false)); - expect(called2, equals(false)); + expect(calledCompleter1.isCompleted, equals(false)); + expect(calledCompleter1.isCompleted, equals(false)); connectedCompleter.complete(); - expect(called1, equals(true)); - expect(called2, equals(true)); + await expectLater(calledCompleter1.future, completes); + await expectLater(calledCompleter2.future, completes); }); - test('re-connects when client sends onBillingServiceDisconnected', () { + test('re-connects when client sends onBillingServiceDisconnected', + () async { connectedCompleter.complete(); + // Ensures all asynchronous connected code finishes. + await manager.runWithClientNonRetryable((_) async {}); + manager.client.callHandler( const MethodCall(onBillingServiceDisconnectedCallback, {'handle': 0}), diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 98219dc9d4e5..ad5a7112f4dd 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -8,8 +8,8 @@ import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_android/src/channel.dart'; import '../stub_in_app_purchase_platform.dart'; +import 'product_details_wrapper_test.dart'; import 'purchase_wrapper_test.dart'; -import 'sku_details_wrapper_test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -115,11 +115,11 @@ void main() { expect(stubPlatform.countPreviousCalls(endConnectionName), equals(1)); }); - group('querySkuDetails', () { + group('queryProductDetails', () { const String queryMethodName = - 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)'; + 'BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)'; - test('handles empty skuDetails', () async { + test('handles empty productDetails', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.developerError; stubPlatform.addResponse(name: queryMethodName, value: { @@ -127,20 +127,22 @@ void main() { 'responseCode': const BillingResponseConverter().toJson(responseCode), 'debugMessage': debugMessage, }, - 'skuDetailsList': >[] + 'productDetailsList': >[] }); - final SkuDetailsResponseWrapper response = await billingClient - .querySkuDetails( - skuType: SkuType.inapp, skusList: ['invalid']); + final ProductDetailsResponseWrapper response = await billingClient + .queryProductDetails(productList: [ + const ProductWrapper( + productId: 'invalid', productType: ProductType.inapp) + ]); const BillingResultWrapper billingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); expect(response.billingResult, equals(billingResult)); - expect(response.skuDetailsList, isEmpty); + expect(response.productDetailsList, isEmpty); }); - test('returns SkuDetailsResponseWrapper', () async { + test('returns ProductDetailsResponseWrapper', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.ok; stubPlatform.addResponse(name: queryMethodName, value: { @@ -148,31 +150,41 @@ void main() { 'responseCode': const BillingResponseConverter().toJson(responseCode), 'debugMessage': debugMessage, }, - 'skuDetailsList': >[buildSkuMap(dummySkuDetails)] + 'productDetailsList': >[ + buildProductMap(dummyOneTimeProductDetails) + ], }); - final SkuDetailsResponseWrapper response = await billingClient - .querySkuDetails( - skuType: SkuType.inapp, skusList: ['invalid']); + final ProductDetailsResponseWrapper response = + await billingClient.queryProductDetails( + productList: [ + const ProductWrapper( + productId: 'invalid', productType: ProductType.inapp), + ], + ); const BillingResultWrapper billingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); expect(response.billingResult, equals(billingResult)); - expect(response.skuDetailsList, contains(dummySkuDetails)); + expect(response.productDetailsList, contains(dummyOneTimeProductDetails)); }); test('handles null method channel response', () async { stubPlatform.addResponse(name: queryMethodName); - final SkuDetailsResponseWrapper response = await billingClient - .querySkuDetails( - skuType: SkuType.inapp, skusList: ['invalid']); + final ProductDetailsResponseWrapper response = + await billingClient.queryProductDetails( + productList: [ + const ProductWrapper( + productId: 'invalid', productType: ProductType.inapp), + ], + ); const BillingResultWrapper billingResult = BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: kInvalidBillingResultErrorMessage); expect(response.billingResult, equals(billingResult)); - expect(response.skuDetailsList, isEmpty); + expect(response.productDetailsList, isEmpty); }); }); @@ -189,26 +201,26 @@ void main() { name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), ); - const SkuDetailsWrapper skuDetails = dummySkuDetails; + const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String profileId = 'hashedProfileId'; expect( await billingClient.launchBillingFlow( - sku: skuDetails.sku, + product: productDetails.productId, accountId: accountId, obfuscatedProfileId: profileId), equals(expectedBillingResult)); final Map arguments = stubPlatform .previousCallMatching(launchMethodName) .arguments as Map; - expect(arguments['sku'], equals(skuDetails.sku)); + expect(arguments['product'], equals(productDetails.productId)); expect(arguments['accountId'], equals(accountId)); expect(arguments['obfuscatedProfileId'], equals(profileId)); }); test( - 'Change subscription throws assertion error `oldSku` and `purchaseToken` has different nullability', + 'Change subscription throws assertion error `oldProduct` and `purchaseToken` has different nullability', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.ok; @@ -218,21 +230,21 @@ void main() { name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), ); - const SkuDetailsWrapper skuDetails = dummySkuDetails; + const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String profileId = 'hashedProfileId'; expect( billingClient.launchBillingFlow( - sku: skuDetails.sku, + product: productDetails.productId, accountId: accountId, obfuscatedProfileId: profileId, - oldSku: dummyOldPurchase.sku), + oldProduct: dummyOldPurchase.products.first), throwsAssertionError); expect( billingClient.launchBillingFlow( - sku: skuDetails.sku, + product: productDetails.productId, accountId: accountId, obfuscatedProfileId: profileId, purchaseToken: dummyOldPurchase.purchaseToken), @@ -250,24 +262,24 @@ void main() { name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), ); - const SkuDetailsWrapper skuDetails = dummySkuDetails; + const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String profileId = 'hashedProfileId'; expect( await billingClient.launchBillingFlow( - sku: skuDetails.sku, + product: productDetails.productId, accountId: accountId, obfuscatedProfileId: profileId, - oldSku: dummyOldPurchase.sku, + oldProduct: dummyOldPurchase.products.first, purchaseToken: dummyOldPurchase.purchaseToken), equals(expectedBillingResult)); final Map arguments = stubPlatform .previousCallMatching(launchMethodName) .arguments as Map; - expect(arguments['sku'], equals(skuDetails.sku)); + expect(arguments['product'], equals(productDetails.productId)); expect(arguments['accountId'], equals(accountId)); - expect(arguments['oldSku'], equals(dummyOldPurchase.sku)); + expect(arguments['oldProduct'], equals(dummyOldPurchase.products.first)); expect( arguments['purchaseToken'], equals(dummyOldPurchase.purchaseToken)); expect(arguments['obfuscatedProfileId'], equals(profileId)); @@ -284,7 +296,7 @@ void main() { name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), ); - const SkuDetailsWrapper skuDetails = dummySkuDetails; + const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String profileId = 'hashedProfileId'; const ProrationMode prorationMode = @@ -292,19 +304,19 @@ void main() { expect( await billingClient.launchBillingFlow( - sku: skuDetails.sku, + product: productDetails.productId, accountId: accountId, obfuscatedProfileId: profileId, - oldSku: dummyOldPurchase.sku, + oldProduct: dummyOldPurchase.products.first, prorationMode: prorationMode, purchaseToken: dummyOldPurchase.purchaseToken), equals(expectedBillingResult)); final Map arguments = stubPlatform .previousCallMatching(launchMethodName) .arguments as Map; - expect(arguments['sku'], equals(skuDetails.sku)); + expect(arguments['product'], equals(productDetails.productId)); expect(arguments['accountId'], equals(accountId)); - expect(arguments['oldSku'], equals(dummyOldPurchase.sku)); + expect(arguments['oldProduct'], equals(dummyOldPurchase.products.first)); expect(arguments['obfuscatedProfileId'], equals(profileId)); expect( arguments['purchaseToken'], equals(dummyOldPurchase.purchaseToken)); @@ -323,7 +335,7 @@ void main() { name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), ); - const SkuDetailsWrapper skuDetails = dummySkuDetails; + const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String profileId = 'hashedProfileId'; const ProrationMode prorationMode = @@ -331,19 +343,19 @@ void main() { expect( await billingClient.launchBillingFlow( - sku: skuDetails.sku, + product: productDetails.productId, accountId: accountId, obfuscatedProfileId: profileId, - oldSku: dummyOldPurchase.sku, + oldProduct: dummyOldPurchase.products.first, prorationMode: prorationMode, purchaseToken: dummyOldPurchase.purchaseToken), equals(expectedBillingResult)); final Map arguments = stubPlatform .previousCallMatching(launchMethodName) .arguments as Map; - expect(arguments['sku'], equals(skuDetails.sku)); + expect(arguments['product'], equals(productDetails.productId)); expect(arguments['accountId'], equals(accountId)); - expect(arguments['oldSku'], equals(dummyOldPurchase.sku)); + expect(arguments['oldProduct'], equals(dummyOldPurchase.products.first)); expect(arguments['obfuscatedProfileId'], equals(profileId)); expect( arguments['purchaseToken'], equals(dummyOldPurchase.purchaseToken)); @@ -360,14 +372,16 @@ void main() { name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), ); - const SkuDetailsWrapper skuDetails = dummySkuDetails; + const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; - expect(await billingClient.launchBillingFlow(sku: skuDetails.sku), + expect( + await billingClient.launchBillingFlow( + product: productDetails.productId), equals(expectedBillingResult)); final Map arguments = stubPlatform .previousCallMatching(launchMethodName) .arguments as Map; - expect(arguments['sku'], equals(skuDetails.sku)); + expect(arguments['product'], equals(productDetails.productId)); expect(arguments['accountId'], isNull); }); @@ -375,9 +389,10 @@ void main() { stubPlatform.addResponse( name: launchMethodName, ); - const SkuDetailsWrapper skuDetails = dummySkuDetails; + const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; expect( - await billingClient.launchBillingFlow(sku: skuDetails.sku), + await billingClient.launchBillingFlow( + product: productDetails.productId), equals(const BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: kInvalidBillingResultErrorMessage))); @@ -386,7 +401,7 @@ void main() { group('queryPurchases', () { const String queryPurchasesMethodName = - 'BillingClient#queryPurchases(String)'; + 'BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)'; test('serializes and deserializes data', () async { const BillingResponse expectedCode = BillingResponse.ok; @@ -406,7 +421,7 @@ void main() { }); final PurchasesResultWrapper response = - await billingClient.queryPurchases(SkuType.inapp); + await billingClient.queryPurchases(ProductType.inapp); expect(response.billingResult, equals(expectedBillingResult)); expect(response.responseCode, equals(expectedCode)); @@ -426,7 +441,7 @@ void main() { }); final PurchasesResultWrapper response = - await billingClient.queryPurchases(SkuType.inapp); + await billingClient.queryPurchases(ProductType.inapp); expect(response.billingResult, equals(expectedBillingResult)); expect(response.responseCode, equals(expectedCode)); @@ -438,7 +453,7 @@ void main() { name: queryPurchasesMethodName, ); final PurchasesResultWrapper response = - await billingClient.queryPurchases(SkuType.inapp); + await billingClient.queryPurchases(ProductType.inapp); expect( response.billingResult, @@ -452,7 +467,7 @@ void main() { group('queryPurchaseHistory', () { const String queryPurchaseHistoryMethodName = - 'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)'; + 'BillingClient#queryPurchaseHistoryAsync(QueryPurchaseHistoryParams, PurchaseHistoryResponseListener)'; test('serializes and deserializes data', () async { const BillingResponse expectedCode = BillingResponse.ok; @@ -474,7 +489,7 @@ void main() { }); final PurchasesHistoryResult response = - await billingClient.queryPurchaseHistory(SkuType.inapp); + await billingClient.queryPurchaseHistory(ProductType.inapp); expect(response.billingResult, equals(expectedBillingResult)); expect(response.purchaseHistoryRecordList, equals(expectedList)); }); @@ -492,7 +507,7 @@ void main() { }); final PurchasesHistoryResult response = - await billingClient.queryPurchaseHistory(SkuType.inapp); + await billingClient.queryPurchaseHistory(ProductType.inapp); expect(response.billingResult, equals(expectedBillingResult)); expect(response.purchaseHistoryRecordList, isEmpty); @@ -503,7 +518,7 @@ void main() { name: queryPurchaseHistoryMethodName, ); final PurchasesHistoryResult response = - await billingClient.queryPurchaseHistory(SkuType.inapp); + await billingClient.queryPurchaseHistory(ProductType.inapp); expect( response.billingResult, @@ -516,7 +531,7 @@ void main() { group('consume purchases', () { const String consumeMethodName = - 'BillingClient#consumeAsync(String, ConsumeResponseListener)'; + 'BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)'; test('consume purchase async success', () async { const BillingResponse expectedCode = BillingResponse.ok; const String debugMessage = 'dummy message'; @@ -549,7 +564,7 @@ void main() { group('acknowledge purchases', () { const String acknowledgeMethodName = - 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; + 'BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; test('acknowledge purchase success', () async { const BillingResponse expectedCode = BillingResponse.ok; const String debugMessage = 'dummy message'; @@ -564,6 +579,7 @@ void main() { expect(billingResult, equals(expectedBillingResult)); }); + test('handles method channel returning null', () async { stubPlatform.addResponse( name: acknowledgeMethodName, @@ -610,47 +626,6 @@ void main() { expect(arguments['feature'], equals('subscriptions')); }); }); - - group('launchPriceChangeConfirmationFlow', () { - const String launchPriceChangeConfirmationFlowMethodName = - 'BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)'; - - const BillingResultWrapper expectedBillingResultPriceChangeConfirmation = - BillingResultWrapper( - responseCode: BillingResponse.ok, - debugMessage: 'dummy message', - ); - - test('serializes and deserializes data', () async { - stubPlatform.addResponse( - name: launchPriceChangeConfirmationFlowMethodName, - value: - buildBillingResultMap(expectedBillingResultPriceChangeConfirmation), - ); - - expect( - await billingClient.launchPriceChangeConfirmationFlow( - sku: dummySkuDetails.sku, - ), - equals(expectedBillingResultPriceChangeConfirmation), - ); - }); - - test('passes sku to launchPriceChangeConfirmationFlow', () async { - stubPlatform.addResponse( - name: launchPriceChangeConfirmationFlowMethodName, - value: - buildBillingResultMap(expectedBillingResultPriceChangeConfirmation), - ); - await billingClient.launchPriceChangeConfirmationFlow( - sku: dummySkuDetails.sku, - ); - final MethodCall call = stubPlatform - .previousCallMatching(launchPriceChangeConfirmationFlowMethodName); - expect(call.arguments, - equals({'sku': dummySkuDetails.sku})); - }); - }); } /// This allows a value of type T or T? to be treated as a value of type T?. diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/product_details_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/product_details_wrapper_test.dart new file mode 100644 index 000000000000..3bd6a497490f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/product_details_wrapper_test.dart @@ -0,0 +1,315 @@ +// 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:in_app_purchase_android/billing_client_wrappers.dart'; +import 'package:in_app_purchase_android/src/types/google_play_product_details.dart'; +import 'package:test/test.dart'; + +const ProductDetailsWrapper dummyOneTimeProductDetails = ProductDetailsWrapper( + description: 'description', + name: 'name', + productId: 'productId', + productType: ProductType.inapp, + title: 'title', + oneTimePurchaseOfferDetails: OneTimePurchaseOfferDetailsWrapper( + formattedPrice: r'$100', + priceAmountMicros: 100000000, + priceCurrencyCode: 'USD', + ), +); + +const ProductDetailsWrapper dummySubscriptionProductDetails = + ProductDetailsWrapper( + description: 'description', + name: 'name', + productId: 'productId', + productType: ProductType.subs, + title: 'title', + subscriptionOfferDetails: [ + SubscriptionOfferDetailsWrapper( + basePlanId: 'basePlanId', + offerTags: ['offerTags'], + offerId: 'offerId', + offerIdToken: 'offerToken', + pricingPhases: [ + PricingPhaseWrapper( + billingCycleCount: 4, + billingPeriod: 'billingPeriod', + formattedPrice: r'$100', + priceAmountMicros: 100000000, + priceCurrencyCode: 'USD', + recurrenceMode: RecurrenceMode.finiteRecurring, + ), + ], + ), + ], +); + +void main() { + group('ProductDetailsWrapper', () { + test('converts one-time purchase from map', () { + const ProductDetailsWrapper expected = dummyOneTimeProductDetails; + final ProductDetailsWrapper parsed = + ProductDetailsWrapper.fromJson(buildProductMap(expected)); + + expect(parsed, equals(expected)); + }); + + test('converts subscription from map', () { + const ProductDetailsWrapper expected = dummySubscriptionProductDetails; + final ProductDetailsWrapper parsed = + ProductDetailsWrapper.fromJson(buildProductMap(expected)); + + expect(parsed, equals(expected)); + }); + }); + + group('ProductDetailsResponseWrapper', () { + test('parsed from map', () { + const BillingResponse responseCode = BillingResponse.ok; + const String debugMessage = 'dummy message'; + final List productsDetails = + [ + dummyOneTimeProductDetails, + dummyOneTimeProductDetails, + ]; + const BillingResultWrapper result = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + final ProductDetailsResponseWrapper expected = + ProductDetailsResponseWrapper( + billingResult: result, productDetailsList: productsDetails); + + final ProductDetailsResponseWrapper parsed = + ProductDetailsResponseWrapper.fromJson({ + 'billingResult': { + 'responseCode': const BillingResponseConverter().toJson(responseCode), + 'debugMessage': debugMessage, + }, + 'productDetailsList': >[ + buildProductMap(dummyOneTimeProductDetails), + buildProductMap(dummyOneTimeProductDetails), + ], + }); + + expect(parsed.billingResult, equals(expected.billingResult)); + expect( + parsed.productDetailsList, containsAll(expected.productDetailsList)); + }); + + test('toProductDetails() should return correct Product object', () { + final ProductDetailsWrapper wrapper = ProductDetailsWrapper.fromJson( + buildProductMap(dummyOneTimeProductDetails)); + final GooglePlayProductDetails product = + GooglePlayProductDetails.fromProductDetails(wrapper).first; + expect(product.title, wrapper.title); + expect(product.description, wrapper.description); + expect(product.id, wrapper.productId); + expect( + product.price, wrapper.oneTimePurchaseOfferDetails?.formattedPrice); + expect(product.productDetails, wrapper); + }); + + test('handles empty list of productDetails', () { + const BillingResponse responseCode = BillingResponse.error; + const String debugMessage = 'dummy message'; + final List productsDetails = + []; + const BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + final ProductDetailsResponseWrapper expected = + ProductDetailsResponseWrapper( + billingResult: billingResult, + productDetailsList: productsDetails); + + final ProductDetailsResponseWrapper parsed = + ProductDetailsResponseWrapper.fromJson({ + 'billingResult': { + 'responseCode': const BillingResponseConverter().toJson(responseCode), + 'debugMessage': debugMessage, + }, + 'productDetailsList': const >[] + }); + + expect(parsed.billingResult, equals(expected.billingResult)); + expect( + parsed.productDetailsList, containsAll(expected.productDetailsList)); + }); + + test('fromJson creates an object with default values', () { + final ProductDetailsResponseWrapper productDetails = + ProductDetailsResponseWrapper.fromJson(const {}); + expect( + productDetails.billingResult, + equals(const BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(productDetails.productDetailsList, isEmpty); + }); + }); + + group('BillingResultWrapper', () { + test('fromJson on empty map creates an object with default values', () { + final BillingResultWrapper billingResult = + BillingResultWrapper.fromJson(const {}); + expect(billingResult.debugMessage, kInvalidBillingResultErrorMessage); + expect(billingResult.responseCode, BillingResponse.error); + }); + + test('fromJson on null creates an object with default values', () { + final BillingResultWrapper billingResult = + BillingResultWrapper.fromJson(null); + expect(billingResult.debugMessage, kInvalidBillingResultErrorMessage); + expect(billingResult.responseCode, BillingResponse.error); + }); + + test('operator == of ProductDetailsWrapper works fine', () { + const ProductDetailsWrapper firstProductDetailsInstance = + ProductDetailsWrapper( + description: 'description', + title: 'title', + productType: ProductType.inapp, + name: 'name', + productId: 'productId', + oneTimePurchaseOfferDetails: OneTimePurchaseOfferDetailsWrapper( + formattedPrice: 'formattedPrice', + priceAmountMicros: 10, + priceCurrencyCode: 'priceCurrencyCode', + ), + subscriptionOfferDetails: [ + SubscriptionOfferDetailsWrapper( + basePlanId: 'basePlanId', + offerTags: ['offerTags'], + offerIdToken: 'offerToken', + pricingPhases: [ + PricingPhaseWrapper( + billingCycleCount: 4, + billingPeriod: 'billingPeriod', + formattedPrice: 'formattedPrice', + priceAmountMicros: 10, + priceCurrencyCode: 'priceCurrencyCode', + recurrenceMode: RecurrenceMode.finiteRecurring, + ), + ], + ), + ], + ); + const ProductDetailsWrapper secondProductDetailsInstance = + ProductDetailsWrapper( + description: 'description', + title: 'title', + productType: ProductType.inapp, + name: 'name', + productId: 'productId', + oneTimePurchaseOfferDetails: OneTimePurchaseOfferDetailsWrapper( + formattedPrice: 'formattedPrice', + priceAmountMicros: 10, + priceCurrencyCode: 'priceCurrencyCode', + ), + subscriptionOfferDetails: [ + SubscriptionOfferDetailsWrapper( + basePlanId: 'basePlanId', + offerTags: ['offerTags'], + offerIdToken: 'offerToken', + pricingPhases: [ + PricingPhaseWrapper( + billingCycleCount: 4, + billingPeriod: 'billingPeriod', + formattedPrice: 'formattedPrice', + priceAmountMicros: 10, + priceCurrencyCode: 'priceCurrencyCode', + recurrenceMode: RecurrenceMode.finiteRecurring, + ), + ], + ), + ], + ); + expect( + firstProductDetailsInstance == secondProductDetailsInstance, isTrue); + }); + + test('operator == of BillingResultWrapper works fine', () { + const BillingResultWrapper firstBillingResultInstance = + BillingResultWrapper( + responseCode: BillingResponse.ok, + debugMessage: 'debugMessage', + ); + const BillingResultWrapper secondBillingResultInstance = + BillingResultWrapper( + responseCode: BillingResponse.ok, + debugMessage: 'debugMessage', + ); + expect(firstBillingResultInstance == secondBillingResultInstance, isTrue); + }); + }); +} + +Map buildProductMap(ProductDetailsWrapper original) { + final Map map = { + 'title': original.title, + 'description': original.description, + 'productId': original.productId, + 'productType': const ProductTypeConverter().toJson(original.productType), + 'name': original.name, + }; + + if (original.oneTimePurchaseOfferDetails != null) { + map.putIfAbsent('oneTimePurchaseOfferDetails', + () => buildOneTimePurchaseMap(original.oneTimePurchaseOfferDetails!)); + } + + if (original.subscriptionOfferDetails != null) { + map.putIfAbsent('subscriptionOfferDetails', + () => buildSubscriptionMapList(original.subscriptionOfferDetails!)); + } + + return map; +} + +Map buildOneTimePurchaseMap( + OneTimePurchaseOfferDetailsWrapper original) { + return { + 'priceAmountMicros': original.priceAmountMicros, + 'priceCurrencyCode': original.priceCurrencyCode, + 'formattedPrice': original.formattedPrice, + }; +} + +List> buildSubscriptionMapList( + List original) { + return original + .map((SubscriptionOfferDetailsWrapper subscriptionOfferDetails) => + buildSubscriptionMap(subscriptionOfferDetails)) + .toList(); +} + +Map buildSubscriptionMap( + SubscriptionOfferDetailsWrapper original) { + return { + 'offerId': original.offerId, + 'basePlanId': original.basePlanId, + 'offerTags': original.offerTags, + 'offerIdToken': original.offerIdToken, + 'pricingPhases': buildPricingPhaseMapList(original.pricingPhases), + }; +} + +List> buildPricingPhaseMapList( + List original) { + return original + .map((PricingPhaseWrapper pricingPhase) => + buildPricingPhaseMap(pricingPhase)) + .toList(); +} + +Map buildPricingPhaseMap(PricingPhaseWrapper original) { + return { + 'formattedPrice': original.formattedPrice, + 'priceCurrencyCode': original.priceCurrencyCode, + 'priceAmountMicros': original.priceAmountMicros, + 'billingCycleCount': original.billingCycleCount, + 'billingPeriod': original.billingPeriod, + 'recurrenceMode': + const RecurrenceModeConverter().toJson(original.recurrenceMode), + }; +} diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/product_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/product_wrapper_test.dart new file mode 100644 index 000000000000..d9fe397b525d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/product_wrapper_test.dart @@ -0,0 +1,30 @@ +// 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:in_app_purchase_android/billing_client_wrappers.dart'; +import 'package:test/test.dart'; + +const ProductWrapper dummyProduct = ProductWrapper( + productId: 'id', + productType: ProductType.inapp, +); + +void main() { + group('ProductWrapper', () { + test('converts product from map', () { + const ProductWrapper expected = dummyProduct; + final ProductWrapper parsed = productFromJson(expected.toJson()); + + expect(parsed, equals(expected)); + }); + }); +} + +ProductWrapper productFromJson(Map serialized) { + return ProductWrapper( + productId: serialized['productId'] as String, + productType: const ProductTypeConverter() + .fromJson(serialized['productType'] as String), + ); +} diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart index 184d9331e6c1..8da1abb8d66e 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart @@ -11,7 +11,7 @@ const PurchaseWrapper dummyPurchase = PurchaseWrapper( packageName: 'packageName', purchaseTime: 0, signature: 'signature', - skus: ['sku'], + products: ['product'], purchaseToken: 'purchaseToken', isAutoRenewing: false, originalJson: '', @@ -22,12 +22,26 @@ const PurchaseWrapper dummyPurchase = PurchaseWrapper( obfuscatedProfileId: 'Profile103', ); +const PurchaseWrapper dummyMultipleProductsPurchase = PurchaseWrapper( + orderId: 'orderId', + packageName: 'packageName', + purchaseTime: 0, + signature: 'signature', + products: ['product', 'product2'], + purchaseToken: 'purchaseToken', + isAutoRenewing: false, + originalJson: '', + developerPayload: 'dummy payload', + isAcknowledged: true, + purchaseState: PurchaseStateWrapper.purchased, +); + const PurchaseWrapper dummyUnacknowledgedPurchase = PurchaseWrapper( orderId: 'orderId', packageName: 'packageName', purchaseTime: 0, signature: 'signature', - skus: ['sku'], + products: ['product'], purchaseToken: 'purchaseToken', isAutoRenewing: false, originalJson: '', @@ -40,7 +54,7 @@ const PurchaseHistoryRecordWrapper dummyPurchaseHistoryRecord = PurchaseHistoryRecordWrapper( purchaseTime: 0, signature: 'signature', - skus: ['sku'], + products: ['product'], purchaseToken: 'purchaseToken', originalJson: '', developerPayload: 'dummy payload', @@ -51,7 +65,7 @@ const PurchaseWrapper dummyOldPurchase = PurchaseWrapper( packageName: 'oldPackageName', purchaseTime: 0, signature: 'oldSignature', - skus: ['oldSku'], + products: ['oldProduct'], purchaseToken: 'oldPurchaseToken', isAutoRenewing: false, originalJson: '', @@ -71,30 +85,45 @@ void main() { }); test('fromPurchase() should return correct PurchaseDetail object', () { - final GooglePlayPurchaseDetails details = - GooglePlayPurchaseDetails.fromPurchase(dummyPurchase); + final List details = + GooglePlayPurchaseDetails.fromPurchase(dummyMultipleProductsPurchase); - expect(details.purchaseID, dummyPurchase.orderId); - expect(details.productID, dummyPurchase.sku); - expect(details.transactionDate, dummyPurchase.purchaseTime.toString()); - expect(details.verificationData, isNotNull); - expect(details.verificationData.source, kIAPSource); - expect(details.verificationData.localVerificationData, - dummyPurchase.originalJson); - expect(details.verificationData.serverVerificationData, - dummyPurchase.purchaseToken); - expect(details.billingClientPurchase, dummyPurchase); - expect(details.pendingCompletePurchase, false); + expect(details[0].purchaseID, dummyMultipleProductsPurchase.orderId); + expect(details[0].productID, dummyMultipleProductsPurchase.products[0]); + expect(details[0].transactionDate, + dummyMultipleProductsPurchase.purchaseTime.toString()); + expect(details[0].verificationData, isNotNull); + expect(details[0].verificationData.source, kIAPSource); + expect(details[0].verificationData.localVerificationData, + dummyMultipleProductsPurchase.originalJson); + expect(details[0].verificationData.serverVerificationData, + dummyMultipleProductsPurchase.purchaseToken); + expect(details[0].billingClientPurchase, dummyMultipleProductsPurchase); + expect(details[0].pendingCompletePurchase, false); + + expect(details[1].purchaseID, dummyMultipleProductsPurchase.orderId); + expect(details[1].productID, dummyMultipleProductsPurchase.products[1]); + expect(details[1].transactionDate, + dummyMultipleProductsPurchase.purchaseTime.toString()); + expect(details[1].verificationData, isNotNull); + expect(details[1].verificationData.source, kIAPSource); + expect(details[1].verificationData.localVerificationData, + dummyMultipleProductsPurchase.originalJson); + expect(details[1].verificationData.serverVerificationData, + dummyMultipleProductsPurchase.purchaseToken); + expect(details[1].billingClientPurchase, dummyMultipleProductsPurchase); + expect(details[1].pendingCompletePurchase, false); }); test( 'fromPurchase() should return set pendingCompletePurchase to true for unacknowledged purchase', () { final GooglePlayPurchaseDetails details = - GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase); + GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase) + .first; expect(details.purchaseID, dummyPurchase.orderId); - expect(details.productID, dummyPurchase.sku); + expect(details.productID, dummyPurchase.products.first); expect(details.transactionDate, dummyPurchase.purchaseTime.toString()); expect(details.verificationData, isNotNull); expect(details.verificationData.source, kIAPSource); @@ -205,7 +234,7 @@ Map buildPurchaseMap(PurchaseWrapper original) { 'packageName': original.packageName, 'purchaseTime': original.purchaseTime, 'signature': original.signature, - 'skus': original.skus, + 'products': original.products, 'purchaseToken': original.purchaseToken, 'isAutoRenewing': original.isAutoRenewing, 'originalJson': original.originalJson, @@ -223,7 +252,7 @@ Map buildPurchaseHistoryRecordMap( return { 'purchaseTime': original.purchaseTime, 'signature': original.signature, - 'skus': original.skus, + 'products': original.products, 'purchaseToken': original.purchaseToken, 'originalJson': original.originalJson, 'developerPayload': original.developerPayload, diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_deprecated_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_deprecated_test.dart deleted file mode 100644 index f27ea02209c4..000000000000 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_deprecated_test.dart +++ /dev/null @@ -1,69 +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. - -// TODO(mvanbeusekom): Remove this file when the deprecated -// `SkuDetailsWrapper.introductoryPriceMicros` field is -// removed. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:in_app_purchase_android/billing_client_wrappers.dart'; - -void main() { - test( - 'Deprecated `introductoryPriceMicros` field reflects parameter from constructor', - () { - const SkuDetailsWrapper skuDetails = SkuDetailsWrapper( - description: 'description', - freeTrialPeriod: 'freeTrialPeriod', - introductoryPrice: 'introductoryPrice', - // ignore: deprecated_member_use_from_same_package - introductoryPriceMicros: '990000', - introductoryPriceCycles: 1, - introductoryPricePeriod: 'introductoryPricePeriod', - price: 'price', - priceAmountMicros: 1000, - priceCurrencyCode: 'priceCurrencyCode', - priceCurrencySymbol: r'$', - sku: 'sku', - subscriptionPeriod: 'subscriptionPeriod', - title: 'title', - type: SkuType.inapp, - originalPrice: 'originalPrice', - originalPriceAmountMicros: 1000, - ); - - expect(skuDetails, isNotNull); - expect(skuDetails.introductoryPriceAmountMicros, 0); - // ignore: deprecated_member_use_from_same_package - expect(skuDetails.introductoryPriceMicros, '990000'); - }); - - test( - '`introductoryPriceAmoutMicros` constructor parameter is reflected by deprecated `introductoryPriceMicros` and `introductoryPriceAmountMicros` fields', - () { - const SkuDetailsWrapper skuDetails = SkuDetailsWrapper( - description: 'description', - freeTrialPeriod: 'freeTrialPeriod', - introductoryPrice: 'introductoryPrice', - introductoryPriceAmountMicros: 990000, - introductoryPriceCycles: 1, - introductoryPricePeriod: 'introductoryPricePeriod', - price: 'price', - priceAmountMicros: 1000, - priceCurrencyCode: 'priceCurrencyCode', - priceCurrencySymbol: r'$', - sku: 'sku', - subscriptionPeriod: 'subscriptionPeriod', - title: 'title', - type: SkuType.inapp, - originalPrice: 'originalPrice', - originalPriceAmountMicros: 1000, - ); - - expect(skuDetails, isNotNull); - expect(skuDetails.introductoryPriceAmountMicros, 990000); - // ignore: deprecated_member_use_from_same_package - expect(skuDetails.introductoryPriceMicros, '990000'); - }); -} diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart deleted file mode 100644 index 2d1436885427..000000000000 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart +++ /dev/null @@ -1,204 +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:in_app_purchase_android/billing_client_wrappers.dart'; -import 'package:in_app_purchase_android/src/types/google_play_product_details.dart'; -import 'package:test/test.dart'; - -const SkuDetailsWrapper dummySkuDetails = SkuDetailsWrapper( - description: 'description', - freeTrialPeriod: 'freeTrialPeriod', - introductoryPrice: 'introductoryPrice', - introductoryPriceAmountMicros: 990000, - introductoryPriceCycles: 1, - introductoryPricePeriod: 'introductoryPricePeriod', - price: 'price', - priceAmountMicros: 1000, - priceCurrencyCode: 'priceCurrencyCode', - priceCurrencySymbol: r'$', - sku: 'sku', - subscriptionPeriod: 'subscriptionPeriod', - title: 'title', - type: SkuType.inapp, - originalPrice: 'originalPrice', - originalPriceAmountMicros: 1000, -); - -void main() { - group('SkuDetailsWrapper', () { - test('converts from map', () { - const SkuDetailsWrapper expected = dummySkuDetails; - final SkuDetailsWrapper parsed = - SkuDetailsWrapper.fromJson(buildSkuMap(expected)); - - expect(parsed, equals(expected)); - }); - }); - - group('SkuDetailsResponseWrapper', () { - test('parsed from map', () { - const BillingResponse responseCode = BillingResponse.ok; - const String debugMessage = 'dummy message'; - final List skusDetails = [ - dummySkuDetails, - dummySkuDetails - ]; - const BillingResultWrapper result = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - final SkuDetailsResponseWrapper expected = SkuDetailsResponseWrapper( - billingResult: result, skuDetailsList: skusDetails); - - final SkuDetailsResponseWrapper parsed = - SkuDetailsResponseWrapper.fromJson({ - 'billingResult': { - 'responseCode': const BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, - 'skuDetailsList': >[ - buildSkuMap(dummySkuDetails), - buildSkuMap(dummySkuDetails) - ] - }); - - expect(parsed.billingResult, equals(expected.billingResult)); - expect(parsed.skuDetailsList, containsAll(expected.skuDetailsList)); - }); - - test('toProductDetails() should return correct Product object', () { - final SkuDetailsWrapper wrapper = - SkuDetailsWrapper.fromJson(buildSkuMap(dummySkuDetails)); - final GooglePlayProductDetails product = - GooglePlayProductDetails.fromSkuDetails(wrapper); - expect(product.title, wrapper.title); - expect(product.description, wrapper.description); - expect(product.id, wrapper.sku); - expect(product.price, wrapper.price); - expect(product.skuDetails, wrapper); - }); - - test('handles empty list of skuDetails', () { - const BillingResponse responseCode = BillingResponse.error; - const String debugMessage = 'dummy message'; - final List skusDetails = []; - const BillingResultWrapper billingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - final SkuDetailsResponseWrapper expected = SkuDetailsResponseWrapper( - billingResult: billingResult, skuDetailsList: skusDetails); - - final SkuDetailsResponseWrapper parsed = - SkuDetailsResponseWrapper.fromJson({ - 'billingResult': { - 'responseCode': const BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, - 'skuDetailsList': const >[] - }); - - expect(parsed.billingResult, equals(expected.billingResult)); - expect(parsed.skuDetailsList, containsAll(expected.skuDetailsList)); - }); - - test('fromJson creates an object with default values', () { - final SkuDetailsResponseWrapper skuDetails = - SkuDetailsResponseWrapper.fromJson(const {}); - expect( - skuDetails.billingResult, - equals(const BillingResultWrapper( - responseCode: BillingResponse.error, - debugMessage: kInvalidBillingResultErrorMessage))); - expect(skuDetails.skuDetailsList, isEmpty); - }); - }); - - group('BillingResultWrapper', () { - test('fromJson on empty map creates an object with default values', () { - final BillingResultWrapper billingResult = - BillingResultWrapper.fromJson(const {}); - expect(billingResult.debugMessage, kInvalidBillingResultErrorMessage); - expect(billingResult.responseCode, BillingResponse.error); - }); - - test('fromJson on null creates an object with default values', () { - final BillingResultWrapper billingResult = - BillingResultWrapper.fromJson(null); - expect(billingResult.debugMessage, kInvalidBillingResultErrorMessage); - expect(billingResult.responseCode, BillingResponse.error); - }); - - test('operator == of SkuDetailsWrapper works fine', () { - const SkuDetailsWrapper firstSkuDetailsInstance = SkuDetailsWrapper( - description: 'description', - freeTrialPeriod: 'freeTrialPeriod', - introductoryPrice: 'introductoryPrice', - introductoryPriceAmountMicros: 990000, - introductoryPriceCycles: 1, - introductoryPricePeriod: 'introductoryPricePeriod', - price: 'price', - priceAmountMicros: 1000, - priceCurrencyCode: 'priceCurrencyCode', - priceCurrencySymbol: r'$', - sku: 'sku', - subscriptionPeriod: 'subscriptionPeriod', - title: 'title', - type: SkuType.inapp, - originalPrice: 'originalPrice', - originalPriceAmountMicros: 1000, - ); - const SkuDetailsWrapper secondSkuDetailsInstance = SkuDetailsWrapper( - description: 'description', - freeTrialPeriod: 'freeTrialPeriod', - introductoryPrice: 'introductoryPrice', - introductoryPriceAmountMicros: 990000, - introductoryPriceCycles: 1, - introductoryPricePeriod: 'introductoryPricePeriod', - price: 'price', - priceAmountMicros: 1000, - priceCurrencyCode: 'priceCurrencyCode', - priceCurrencySymbol: r'$', - sku: 'sku', - subscriptionPeriod: 'subscriptionPeriod', - title: 'title', - type: SkuType.inapp, - originalPrice: 'originalPrice', - originalPriceAmountMicros: 1000, - ); - expect(firstSkuDetailsInstance == secondSkuDetailsInstance, isTrue); - }); - - test('operator == of BillingResultWrapper works fine', () { - const BillingResultWrapper firstBillingResultInstance = - BillingResultWrapper( - responseCode: BillingResponse.ok, - debugMessage: 'debugMessage', - ); - const BillingResultWrapper secondBillingResultInstance = - BillingResultWrapper( - responseCode: BillingResponse.ok, - debugMessage: 'debugMessage', - ); - expect(firstBillingResultInstance == secondBillingResultInstance, isTrue); - }); - }); -} - -Map buildSkuMap(SkuDetailsWrapper original) { - return { - 'description': original.description, - 'freeTrialPeriod': original.freeTrialPeriod, - 'introductoryPrice': original.introductoryPrice, - 'introductoryPriceAmountMicros': original.introductoryPriceAmountMicros, - 'introductoryPriceCycles': original.introductoryPriceCycles, - 'introductoryPricePeriod': original.introductoryPricePeriod, - 'price': original.price, - 'priceAmountMicros': original.priceAmountMicros, - 'priceCurrencyCode': original.priceCurrencyCode, - 'priceCurrencySymbol': original.priceCurrencySymbol, - 'sku': original.sku, - 'subscriptionPeriod': original.subscriptionPeriod, - 'title': original.title, - 'type': original.type.toString().substring(8), - 'originalPrice': original.originalPrice, - 'originalPriceAmountMicros': original.originalPriceAmountMicros, - }; -} diff --git a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart index 1b61f53b0d38..cae9396431a5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart @@ -44,7 +44,7 @@ void main() { group('consume purchases', () { const String consumeMethodName = - 'BillingClient#consumeAsync(String, ConsumeResponseListener)'; + 'BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)'; test('consume purchase async success', () async { const BillingResponse expectedCode = BillingResponse.ok; const String debugMessage = 'dummy message'; @@ -56,7 +56,7 @@ void main() { ); final BillingResultWrapper billingResultWrapper = await iapAndroidPlatformAddition.consumePurchase( - GooglePlayPurchaseDetails.fromPurchase(dummyPurchase)); + GooglePlayPurchaseDetails.fromPurchase(dummyPurchase).first); expect(billingResultWrapper, equals(expectedBillingResult)); }); @@ -64,7 +64,8 @@ void main() { group('queryPastPurchase', () { group('queryPurchaseDetails', () { - const String queryMethodName = 'BillingClient#queryPurchases(String)'; + const String queryMethodName = + 'BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)'; test('handles error', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.developerError; @@ -86,7 +87,7 @@ void main() { expect(response.error!.source, kIAPSource); }); - test('returns SkuDetailsResponseWrapper', () async { + test('returns ProductDetailsResponseWrapper', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.ok; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( @@ -101,7 +102,7 @@ void main() { ] }); - // Since queryPastPurchases makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead + // Since queryPastPurchases makes 2 platform method calls (one for each ProductType), the result will contain 2 dummyWrapper instead // of 1. final QueryPurchaseDetailsResponse response = await iapAndroidPlatformAddition.queryPastPurchases(); @@ -173,47 +174,6 @@ void main() { expect(arguments['feature'], equals('subscriptions')); }); }); - - group('launchPriceChangeConfirmationFlow', () { - const String launchPriceChangeConfirmationFlowMethodName = - 'BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)'; - const String dummySku = 'sku'; - - const BillingResultWrapper expectedBillingResultPriceChangeConfirmation = - BillingResultWrapper( - responseCode: BillingResponse.ok, - debugMessage: 'dummy message', - ); - - test('serializes and deserializes data', () async { - stubPlatform.addResponse( - name: launchPriceChangeConfirmationFlowMethodName, - value: - buildBillingResultMap(expectedBillingResultPriceChangeConfirmation), - ); - - expect( - await iapAndroidPlatformAddition.launchPriceChangeConfirmationFlow( - sku: dummySku, - ), - equals(expectedBillingResultPriceChangeConfirmation), - ); - }); - - test('passes sku to launchPriceChangeConfirmationFlow', () async { - stubPlatform.addResponse( - name: launchPriceChangeConfirmationFlowMethodName, - value: - buildBillingResultMap(expectedBillingResultPriceChangeConfirmation), - ); - await iapAndroidPlatformAddition.launchPriceChangeConfirmationFlow( - sku: dummySku, - ); - final MethodCall call = stubPlatform - .previousCallMatching(launchPriceChangeConfirmationFlowMethodName); - expect(call.arguments, equals({'sku': dummySku})); - }); - }); } /// This allows a value of type T or T? to be treated as a value of type T?. diff --git a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart index a679def27d51..205e1b0653b4 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart @@ -12,8 +12,8 @@ import 'package:in_app_purchase_android/in_app_purchase_android.dart'; import 'package:in_app_purchase_android/src/channel.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'billing_client_wrappers/product_details_wrapper_test.dart'; import 'billing_client_wrappers/purchase_wrapper_test.dart'; -import 'billing_client_wrappers/sku_details_wrapper_test.dart'; import 'stub_in_app_purchase_platform.dart'; void main() { @@ -25,7 +25,7 @@ void main() { 'BillingClient#startConnection(BillingClientStateListener)'; const String endConnectionCall = 'BillingClient#endConnection()'; const String acknowledgePurchaseCall = - 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; + 'BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; const String onBillingServiceDisconnectedCallback = 'BillingClientStateListener#onBillingServiceDisconnected()'; @@ -90,7 +90,8 @@ void main() { name: acknowledgePurchaseCall, value: okValue), ); final PurchaseDetails purchase = - GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase); + GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase) + .first; final BillingResultWrapper result = await iapAndroidPlatform.completePurchase(purchase); expect( @@ -114,18 +115,18 @@ void main() { }); }); - group('querySkuDetails', () { + group('queryProductDetails', () { const String queryMethodName = - 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)'; + 'BillingClient#queryProductDetailsAsync(QueryProductDetailsParams, ProductDetailsResponseListener)'; - test('handles empty skuDetails', () async { + test('handles empty productDetails', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.ok; const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse(name: queryMethodName, value: { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'skuDetailsList': >[], + 'productDetailsList': >[], }); final ProductDetailsResponse response = @@ -140,16 +141,22 @@ void main() { responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse(name: queryMethodName, value: { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'skuDetailsList': >[buildSkuMap(dummySkuDetails)] + 'productDetailsList': >[ + buildProductMap(dummyOneTimeProductDetails) + ] }); - // Since queryProductDetails makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead + // Since queryProductDetails makes 2 platform method calls (one for each ProductType), the result will contain 2 dummyWrapper instead // of 1. final ProductDetailsResponse response = await iapAndroidPlatform.queryProductDetails({'valid'}); - expect(response.productDetails.first.title, dummySkuDetails.title); + expect(response.productDetails.first.title, + dummyOneTimeProductDetails.title); expect(response.productDetails.first.description, - dummySkuDetails.description); - expect(response.productDetails.first.price, dummySkuDetails.price); + dummyOneTimeProductDetails.description); + expect( + response.productDetails.first.price, + dummyOneTimeProductDetails + .oneTimePurchaseOfferDetails?.formattedPrice); expect(response.productDetails.first.currencySymbol, r'$'); }); @@ -160,9 +167,11 @@ void main() { responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse(name: queryMethodName, value: { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'skuDetailsList': >[buildSkuMap(dummySkuDetails)] + 'productDetailsList': >[ + buildProductMap(dummyOneTimeProductDetails) + ] }); - // Since queryProductDetails makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead + // Since queryProductDetails makes 2 platform method calls (one for each ProductType), the result will contain 2 dummyWrapper instead // of 1. final ProductDetailsResponse response = await iapAndroidPlatform.queryProductDetails({'invalid'}); @@ -178,8 +187,8 @@ void main() { value: { 'responseCode': const BillingResponseConverter().toJson(responseCode), - 'skuDetailsList': >[ - buildSkuMap(dummySkuDetails) + 'productDetailsList': >[ + buildProductMap(dummyOneTimeProductDetails) ] }, additionalStepBeforeReturn: (dynamic _) { @@ -189,7 +198,7 @@ void main() { details: {'info': 'error_info'}, ); }); - // Since queryProductDetails makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead + // Since queryProductDetails makes 2 platform method calls (one for each ProductType), the result will contain 2 dummyWrapper instead // of 1. final ProductDetailsResponse response = await iapAndroidPlatform.queryProductDetails({'invalid'}); @@ -204,7 +213,8 @@ void main() { }); group('restorePurchases', () { - const String queryMethodName = 'BillingClient#queryPurchases(String)'; + const String queryMethodName = + 'BillingClient#queryPurchasesAsync(QueryPurchaseParams, PurchaseResponseListener)'; test('handles error', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.developerError; @@ -266,7 +276,7 @@ void main() { ); }); - test('returns SkuDetailsResponseWrapper', () async { + test('returns ProductDetailsResponseWrapper', () async { final Completer> completer = Completer>(); final Stream> stream = @@ -294,7 +304,7 @@ void main() { }); // Since queryPastPurchases makes 2 platform method calls (one for each - // SkuType), the result will contain 2 dummyPurchase instances instead + // ProductType), the result will contain 2 dummyPurchase instances instead // of 1. await iapAndroidPlatform.restorePurchases(); final List restoredPurchases = await completer.future; @@ -304,7 +314,7 @@ void main() { final GooglePlayPurchaseDetails purchase = element as GooglePlayPurchaseDetails; - expect(purchase.productID, dummyPurchase.sku); + expect(purchase.productID, dummyPurchase.products.first); expect(purchase.purchaseID, dummyPurchase.orderId); expect(purchase.verificationData.localVerificationData, dummyPurchase.originalJson); @@ -322,10 +332,10 @@ void main() { const String launchMethodName = 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)'; const String consumeMethodName = - 'BillingClient#consumeAsync(String, ConsumeResponseListener)'; + 'BillingClient#consumeAsync(ConsumeParams, ConsumeResponseListener)'; test('buy non consumable, serializes and deserializes data', () async { - const SkuDetailsWrapper skuDetails = dummySkuDetails; + const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String debugMessage = 'dummy message'; const BillingResponse sentCode = BillingResponse.ok; @@ -344,7 +354,7 @@ void main() { 'purchasesList': [ { 'orderId': 'orderID1', - 'skus': [skuDetails.sku], + 'products': [productDetails.productId], 'isAutoRenewing': false, 'packageName': 'package', 'purchaseTime': 1231231231, @@ -370,7 +380,8 @@ void main() { subscription.cancel(); }, onDone: () {}); final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam( - productDetails: GooglePlayProductDetails.fromSkuDetails(skuDetails), + productDetails: + GooglePlayProductDetails.fromProductDetails(productDetails).first, applicationUserName: accountId); final bool launchResult = await iapAndroidPlatform.buyNonConsumable( purchaseParam: purchaseParam); @@ -379,11 +390,11 @@ void main() { expect(launchResult, isTrue); expect(result.purchaseID, 'orderID1'); expect(result.status, PurchaseStatus.purchased); - expect(result.productID, dummySkuDetails.sku); + expect(result.productID, productDetails.productId); }); test('handles an error with an empty purchases list', () async { - const SkuDetailsWrapper skuDetails = dummySkuDetails; + const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String debugMessage = 'dummy message'; const BillingResponse sentCode = BillingResponse.error; @@ -414,7 +425,8 @@ void main() { subscription.cancel(); }, onDone: () {}); final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam( - productDetails: GooglePlayProductDetails.fromSkuDetails(skuDetails), + productDetails: + GooglePlayProductDetails.fromProductDetails(productDetails).first, applicationUserName: accountId); await iapAndroidPlatform.buyNonConsumable(purchaseParam: purchaseParam); final PurchaseDetails result = await completer.future; @@ -427,7 +439,7 @@ void main() { test('buy consumable with auto consume, serializes and deserializes data', () async { - const SkuDetailsWrapper skuDetails = dummySkuDetails; + const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String debugMessage = 'dummy message'; const BillingResponse sentCode = BillingResponse.ok; @@ -446,7 +458,7 @@ void main() { 'purchasesList': [ { 'orderId': 'orderID1', - 'skus': [skuDetails.sku], + 'products': [productDetails.productId], 'isAutoRenewing': false, 'packageName': 'package', 'purchaseTime': 1231231231, @@ -487,7 +499,8 @@ void main() { subscription.cancel(); }, onDone: () {}); final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam( - productDetails: GooglePlayProductDetails.fromSkuDetails(skuDetails), + productDetails: + GooglePlayProductDetails.fromProductDetails(productDetails).first, applicationUserName: accountId); final bool launchResult = await iapAndroidPlatform.buyConsumable(purchaseParam: purchaseParam); @@ -515,8 +528,9 @@ void main() { final bool result = await iapAndroidPlatform.buyNonConsumable( purchaseParam: GooglePlayPurchaseParam( - productDetails: - GooglePlayProductDetails.fromSkuDetails(dummySkuDetails))); + productDetails: GooglePlayProductDetails.fromProductDetails( + dummyOneTimeProductDetails) + .first)); // Verify that the failure has been converted and returned expect(result, isFalse); @@ -535,15 +549,16 @@ void main() { final bool result = await iapAndroidPlatform.buyConsumable( purchaseParam: GooglePlayPurchaseParam( - productDetails: - GooglePlayProductDetails.fromSkuDetails(dummySkuDetails))); + productDetails: GooglePlayProductDetails.fromProductDetails( + dummyOneTimeProductDetails) + .first)); // Verify that the failure has been converted and returned expect(result, isFalse); }); test('adds consumption failures to PurchaseDetails objects', () async { - const SkuDetailsWrapper skuDetails = dummySkuDetails; + const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String debugMessage = 'dummy message'; const BillingResponse sentCode = BillingResponse.ok; @@ -561,7 +576,7 @@ void main() { 'purchasesList': [ { 'orderId': 'orderID1', - 'skus': [skuDetails.sku], + 'products': [productDetails.productId], 'isAutoRenewing': false, 'packageName': 'package', 'purchaseTime': 1231231231, @@ -602,7 +617,8 @@ void main() { subscription.cancel(); }, onDone: () {}); final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam( - productDetails: GooglePlayProductDetails.fromSkuDetails(skuDetails), + productDetails: + GooglePlayProductDetails.fromProductDetails(productDetails).first, applicationUserName: accountId); await iapAndroidPlatform.buyConsumable(purchaseParam: purchaseParam); @@ -620,7 +636,7 @@ void main() { test( 'buy consumable without auto consume, consume api should not receive calls', () async { - const SkuDetailsWrapper skuDetails = dummySkuDetails; + const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String debugMessage = 'dummy message'; const BillingResponse sentCode = BillingResponse.developerError; @@ -639,7 +655,7 @@ void main() { 'purchasesList': [ { 'orderId': 'orderID1', - 'skus': [skuDetails.sku], + 'products': [productDetails.productId], 'isAutoRenewing': false, 'packageName': 'package', 'purchaseTime': 1231231231, @@ -677,7 +693,8 @@ void main() { subscription.cancel(); }, onDone: () {}); final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam( - productDetails: GooglePlayProductDetails.fromSkuDetails(skuDetails), + productDetails: + GooglePlayProductDetails.fromProductDetails(productDetails).first, applicationUserName: accountId); await iapAndroidPlatform.buyConsumable( purchaseParam: purchaseParam, autoConsume: false); @@ -687,7 +704,7 @@ void main() { test( 'should get canceled purchase status when response code is BillingResponse.userCanceled', () async { - const SkuDetailsWrapper skuDetails = dummySkuDetails; + const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String debugMessage = 'dummy message'; const BillingResponse sentCode = BillingResponse.userCanceled; @@ -705,7 +722,7 @@ void main() { 'purchasesList': [ { 'orderId': 'orderID1', - 'sku': skuDetails.sku, + 'products': [productDetails.productId], 'isAutoRenewing': false, 'packageName': 'package', 'purchaseTime': 1231231231, @@ -746,7 +763,8 @@ void main() { subscription.cancel(); }, onDone: () {}); final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam( - productDetails: GooglePlayProductDetails.fromSkuDetails(skuDetails), + productDetails: + GooglePlayProductDetails.fromProductDetails(productDetails).first, applicationUserName: accountId); await iapAndroidPlatform.buyConsumable(purchaseParam: purchaseParam); @@ -759,7 +777,7 @@ void main() { test( 'should get purchased purchase status when upgrading subscription by deferred proration mode', () async { - const SkuDetailsWrapper skuDetails = dummySkuDetails; + const ProductDetailsWrapper productDetails = dummyOneTimeProductDetails; const String accountId = 'hashedAccountId'; const String debugMessage = 'dummy message'; const BillingResponse sentCode = BillingResponse.ok; @@ -790,11 +808,13 @@ void main() { subscription.cancel(); }, onDone: () {}); final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam( - productDetails: GooglePlayProductDetails.fromSkuDetails(skuDetails), + productDetails: + GooglePlayProductDetails.fromProductDetails(productDetails).first, applicationUserName: accountId, changeSubscriptionParam: ChangeSubscriptionParam( oldPurchaseDetails: GooglePlayPurchaseDetails.fromPurchase( - dummyUnacknowledgedPurchase), + dummyUnacknowledgedPurchase) + .first, prorationMode: ProrationMode.deferred, )); await iapAndroidPlatform.buyNonConsumable(purchaseParam: purchaseParam); @@ -806,7 +826,7 @@ void main() { group('complete purchase', () { const String completeMethodName = - 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; + 'BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; test('complete purchase success', () async { const BillingResponse expectedCode = BillingResponse.ok; const String debugMessage = 'dummy message'; @@ -817,7 +837,8 @@ void main() { value: buildBillingResultMap(expectedBillingResult), ); final PurchaseDetails purchaseDetails = - GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase); + GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase) + .first; final Completer completer = Completer(); purchaseDetails.status = PurchaseStatus.purchased; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md index c639d58ad2a4..25fed8d617d6 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 1.3.4 +* Removes obsolete null checks on non-nullable values. * Updates minimum Flutter version to 3.3. * Aligns Dart and Flutter SDK constraints. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_exception.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_exception.dart index 0a89a6e39a5e..8bac26f17c7d 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_exception.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_exception.dart @@ -11,7 +11,7 @@ class InAppPurchaseException implements Exception { required this.source, required this.code, this.message, - }) : assert(code != null); + }); /// An error code. final String code; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml index 868f7587b752..ae851c7609c9 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/in_app_purcha issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%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: 1.3.3 +version: 1.3.4 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md index 5692382fa9ed..4c13eabc32f7 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.3.6+4 + +* Removes obsolete null checks on non-nullable values. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 0.3.6+3 * Adds a null check, to prevent a new diagnostic. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/project.pbxproj index 7e30d1fa4c1d..761737047418 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -379,6 +379,7 @@ }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/example/pubspec.yaml index 326b4595a646..494e49161593 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the in_app_purchase_storekit plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart index 0e5e420ece85..36d706ce5fe9 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart @@ -131,12 +131,10 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { invalidProductIdentifiers: identifiers.toList()); } List productDetails = []; - if (response.products != null) { - productDetails = response.products - .map((SKProductWrapper productWrapper) => - AppStoreProductDetails.fromSKProduct(productWrapper)) - .toList(); - } + productDetails = response.products + .map((SKProductWrapper productWrapper) => + AppStoreProductDetails.fromSKProduct(productWrapper)) + .toList(); List invalidIdentifiers = response.invalidProductIdentifiers; if (productDetails.isEmpty) { invalidIdentifiers = identifiers.toList(); diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 859946b557bf..361cb3af2d9e 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -398,7 +398,6 @@ class SKPaymentWrapper { /// types of all of the members on this class. The `map` parameter must not be /// null. factory SKPaymentWrapper.fromJson(Map map) { - assert(map != null); return _$SKPaymentWrapperFromJson(map); } @@ -515,7 +514,6 @@ class SKPaymentDiscountWrapper { /// The map needs to have named string keys with values matching the names and /// types of all of the members on this class. factory SKPaymentDiscountWrapper.fromJson(Map map) { - assert(map != null); return _$SKPaymentDiscountWrapperFromJson(map); } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml index 79a41cbab264..48a6e34e8610 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_storekit description: An implementation for the iOS and macOS platforms of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework. repository: https://github.com/flutter/packages/tree/main/packages/in_app_purchase/in_app_purchase_storekit issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.3.6+3 +version: 0.3.6+4 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/ios_platform_images/CHANGELOG.md b/packages/ios_platform_images/CHANGELOG.md index cb089dacdd9d..e54932a4d45d 100644 --- a/packages/ios_platform_images/CHANGELOG.md +++ b/packages/ios_platform_images/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. * Aligns Dart and Flutter SDK constraints. ## 0.2.2+1 diff --git a/packages/ios_platform_images/example/pubspec.yaml b/packages/ios_platform_images/example/pubspec.yaml index 71f253a42f67..0de91494f15b 100644 --- a/packages/ios_platform_images/example/pubspec.yaml +++ b/packages/ios_platform_images/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the ios_platform_images plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: cupertino_icons: ^1.0.2 diff --git a/packages/ios_platform_images/example/test/widget_test.dart b/packages/ios_platform_images/example/test/widget_test.dart index f3cd4c68b65b..5b50a0f001e2 100644 --- a/packages/ios_platform_images/example/test/widget_test.dart +++ b/packages/ios_platform_images/example/test/widget_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -16,8 +14,7 @@ void main() { expect( find.byWidgetPredicate( - (Widget widget) => - widget is Image && (!Platform.isIOS || widget.image != null), + (Widget widget) => widget is Image, ), findsOneWidget, ); diff --git a/packages/local_auth/local_auth/example/.pluginToolsConfig.yaml b/packages/local_auth/local_auth/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/local_auth/local_auth/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md index 6752fc53d061..b5747260f82b 100644 --- a/packages/local_auth/local_auth_android/CHANGELOG.md +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 1.0.28 + +* Removes unused resources as indicated by Android lint warnings. + ## 1.0.27 * Fixes compatibility with AGP versions older than 4.2. diff --git a/packages/local_auth/local_auth_android/android/build.gradle b/packages/local_auth/local_auth_android/android/build.gradle index 6c1dc9c17611..47af7c3dccb9 100644 --- a/packages/local_auth/local_auth_android/android/build.gradle +++ b/packages/local_auth/local_auth_android/android/build.gradle @@ -42,7 +42,6 @@ android { checkAllWarnings true warningsAsErrors true disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' - baseline file("lint-baseline.xml") } diff --git a/packages/local_auth/local_auth_android/android/lint-baseline.xml b/packages/local_auth/local_auth_android/android/lint-baseline.xml deleted file mode 100644 index a695371a9996..000000000000 --- a/packages/local_auth/local_auth_android/android/lint-baseline.xml +++ /dev/null @@ -1,246 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_initial_icon.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_initial_icon.xml deleted file mode 100644 index 610f7cd92bae..000000000000 --- a/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_initial_icon.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - diff --git a/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_success_icon.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_success_icon.xml deleted file mode 100644 index 78e42a8d7f7b..000000000000 --- a/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_success_icon.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - diff --git a/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_warning_icon.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_warning_icon.xml deleted file mode 100644 index 5020d067e1e0..000000000000 --- a/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_warning_icon.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - diff --git a/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_done_white_24dp.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_done_white_24dp.xml deleted file mode 100644 index 99caef98dd7b..000000000000 --- a/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_done_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_fingerprint_white_24dp.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_fingerprint_white_24dp.xml deleted file mode 100644 index 42d8eef7e929..000000000000 --- a/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_fingerprint_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_priority_high_white_24dp.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_priority_high_white_24dp.xml deleted file mode 100644 index f32d4a4f4d79..000000000000 --- a/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_priority_high_white_24dp.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml b/packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml index 8c932d4d19c8..902635ef5436 100644 --- a/packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml +++ b/packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml @@ -1,26 +1,30 @@ - - + android:orientation="vertical" + android:paddingLeft="24dp" + android:paddingRight="24dp"> + + + + diff --git a/packages/local_auth/local_auth_android/android/src/main/res/layout/scan_fp.xml b/packages/local_auth/local_auth_android/android/src/main/res/layout/scan_fp.xml deleted file mode 100644 index a99dd6a607bb..000000000000 --- a/packages/local_auth/local_auth_android/android/src/main/res/layout/scan_fp.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - diff --git a/packages/local_auth/local_auth_android/android/src/main/res/values/colors.xml b/packages/local_auth/local_auth_android/android/src/main/res/values/colors.xml index c011fe33787c..0fd4d38020c0 100644 --- a/packages/local_auth/local_auth_android/android/src/main/res/values/colors.xml +++ b/packages/local_auth/local_auth_android/android/src/main/res/values/colors.xml @@ -1,9 +1,6 @@ - #E53935 - #BDBDBD - #43A047 #212121 #757575 diff --git a/packages/local_auth/local_auth_android/android/src/main/res/values/dimens.xml b/packages/local_auth/local_auth_android/android/src/main/res/values/dimens.xml index 678faebffd61..13a68e95a495 100644 --- a/packages/local_auth/local_auth_android/android/src/main/res/values/dimens.xml +++ b/packages/local_auth/local_auth_android/android/src/main/res/values/dimens.xml @@ -1,5 +1,4 @@ - 14sp 16sp 20sp diff --git a/packages/local_auth/local_auth_android/example/.pluginToolsConfig.yaml b/packages/local_auth/local_auth_android/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/local_auth/local_auth_android/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/local_auth/local_auth_android/example/pubspec.yaml b/packages/local_auth/local_auth_android/example/pubspec.yaml index 9a202edade23..d5801103e818 100644 --- a/packages/local_auth/local_auth_android/example/pubspec.yaml +++ b/packages/local_auth/local_auth_android/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the local_auth_android plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml index a1843ec40c7f..ccdb5261add6 100644 --- a/packages/local_auth/local_auth_android/pubspec.yaml +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -2,11 +2,11 @@ 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.27 +version: 1.0.28 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/local_auth/local_auth_ios/CHANGELOG.md b/packages/local_auth/local_auth_ios/CHANGELOG.md index 165a3c668e20..2d47acf2f013 100644 --- a/packages/local_auth/local_auth_ios/CHANGELOG.md +++ b/packages/local_auth/local_auth_ios/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.1.3 + +* Migrates internal implementation to Pigeon. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 1.1.2 + +* Internal refactoring for maintainability. + ## 1.1.1 * Clarifies explanation of endorsement in README. diff --git a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m index 8ca4c4ecbb7e..dff4c4c11ad1 100644 --- a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m +++ b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m @@ -4,22 +4,41 @@ @import LocalAuthentication; @import XCTest; +@import local_auth_ios; #import -#if __has_include() -#import -#else -@import local_auth_ios; -#endif +// Set a long timeout to avoid flake due to slow CI. +static const NSTimeInterval kTimeout = 30.0; -// Private API needed for tests. -@interface FLTLocalAuthPlugin (Test) -- (void)setAuthContextOverrides:(NSArray *)authContexts; +/** + * A context factory that returns preset contexts. + */ +@interface StubAuthContextFactory : NSObject +@property(copy, nonatomic) NSMutableArray *contexts; +- (instancetype)initWithContexts:(NSArray *)contexts; @end -// Set a long timeout to avoid flake due to slow CI. -static const NSTimeInterval kTimeout = 30.0; +@implementation StubAuthContextFactory + +- (instancetype)initWithContexts:(NSArray *)contexts { + self = [super init]; + if (self) { + _contexts = [contexts mutableCopy]; + } + return self; +} + +- (LAContext *)createAuthContext { + NSAssert(self.contexts.count > 0, @"Insufficient test contexts provided"); + LAContext *context = [self.contexts firstObject]; + [self.contexts removeObjectAtIndex:0]; + return context; +} + +@end + +#pragma mark - @interface FLTLocalAuthPluginTests : XCTestCase @end @@ -31,12 +50,13 @@ - (void)setUp { } - (void)testSuccessfullAuthWithBiometrics { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - NSString *reason = @"a reason"; + FLAAuthStrings *strings = [self createAuthStrings]; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -49,33 +69,32 @@ - (void)testSuccessfullAuthWithBiometrics { reply(YES, nil); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(YES), - @"localizedReason" : reason, - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@YES + sticky:@NO + useErrorDialogs:@NO] + strings:strings + completion:^(FLAAuthResultDetails *_Nullable resultDetails, + FlutterError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(resultDetails.result, FLAAuthResultSuccess); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testSuccessfullAuthWithoutBiometrics { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString *reason = @"a reason"; + FLAAuthStrings *strings = [self createAuthStrings]; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -88,33 +107,32 @@ - (void)testSuccessfullAuthWithoutBiometrics { reply(YES, nil); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO + sticky:@NO + useErrorDialogs:@NO] + strings:strings + completion:^(FLAAuthResultDetails *_Nullable resultDetails, + FlutterError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(resultDetails.result, FLAAuthResultSuccess); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testFailedAuthWithBiometrics { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - NSString *reason = @"a reason"; + FLAAuthStrings *strings = [self createAuthStrings]; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -127,32 +145,36 @@ - (void)testFailedAuthWithBiometrics { reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(YES), - @"localizedReason" : reason, - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@YES + sticky:@NO + useErrorDialogs:@NO] + strings:strings + completion:^(FLAAuthResultDetails *_Nullable resultDetails, + FlutterError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + // TODO(stuartmorgan): Fix this; this was the pre-Pigeon-migration + // behavior, so is preserved as part of the migration, but a failed + // authentication should return failure, not an error that results in a + // PlatformException. + XCTAssertEqual(resultDetails.result, FLAAuthResultErrorNotAvailable); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testFailedWithUnknownErrorCode { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString *reason = @"a reason"; + FLAAuthStrings *strings = [self createAuthStrings]; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -165,32 +187,32 @@ - (void)testFailedWithUnknownErrorCode { reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO + sticky:@NO + useErrorDialogs:@NO] + strings:strings + completion:^(FLAAuthResultDetails *_Nullable resultDetails, + FlutterError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(resultDetails.result, FLAAuthResultErrorNotAvailable); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testSystemCancelledWithoutStickyAuth { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString *reason = @"a reason"; + FLAAuthStrings *strings = [self createAuthStrings]; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -203,34 +225,32 @@ - (void)testSystemCancelledWithoutStickyAuth { reply(NO, [NSError errorWithDomain:@"error" code:LAErrorSystemCancel userInfo:nil]); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - @"stickyAuth" : @(NO) - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertFalse([result boolValue]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO + sticky:@NO + useErrorDialogs:@NO] + strings:strings + completion:^(FLAAuthResultDetails *_Nullable resultDetails, + FlutterError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(resultDetails.result, FLAAuthResultFailure); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testFailedAuthWithoutBiometrics { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString *reason = @"a reason"; + FLAAuthStrings *strings = [self createAuthStrings]; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -243,33 +263,37 @@ - (void)testFailedAuthWithoutBiometrics { reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[FlutterError class]]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO + sticky:@NO + useErrorDialogs:@NO] + strings:strings + completion:^(FLAAuthResultDetails *_Nullable resultDetails, + FlutterError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + // TODO(stuartmorgan): Fix this; this was the pre-Pigeon-migration + // behavior, so is preserved as part of the migration, but a failed + // authentication should return failure, not an error that results in a + // PlatformException. + XCTAssertEqual(resultDetails.result, FLAAuthResultErrorNotAvailable); + XCTAssertNil(error); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testLocalizedFallbackTitle { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString *reason = @"a reason"; - NSString *localizedFallbackTitle = @"a title"; + FLAAuthStrings *strings = [self createAuthStrings]; + strings.localizedFallbackTitle = @"a title"; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -282,33 +306,32 @@ - (void)testLocalizedFallbackTitle { reply(YES, nil); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - @"localizedFallbackTitle" : localizedFallbackTitle, - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO + sticky:@NO + useErrorDialogs:@NO] + strings:strings + completion:^(FLAAuthResultDetails *_Nullable resultDetails, + FlutterError *_Nullable error) { + OCMVerify([mockAuthContext + setLocalizedFallbackTitle:strings.localizedFallbackTitle]); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testSkippedLocalizedFallbackTitle { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString *reason = @"a reason"; + FLAAuthStrings *strings = [self createAuthStrings]; + strings.localizedFallbackTitle = nil; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not @@ -321,50 +344,42 @@ - (void)testSkippedLocalizedFallbackTitle { reply(YES, nil); }); }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) .andDo(backgroundThreadReplyCaller); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - }]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); - [expectation fulfill]; - }]; + [plugin authenticateWithOptions:[FLAAuthOptions makeWithBiometricOnly:@NO + sticky:@NO + useErrorDialogs:@NO] + strings:strings + completion:^(FLAAuthResultDetails *_Nullable resultDetails, + FlutterError *_Nullable error) { + OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testDeviceSupportsBiometrics_withEnrolledHardware { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FlutterError *error; + NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; + XCTAssertTrue([result boolValue]); + XCTAssertNil(error); } - (void)testDeviceSupportsBiometrics_withNonEnrolledHardware { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { @@ -381,24 +396,17 @@ - (void)testDeviceSupportsBiometrics_withNonEnrolledHardware { error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) .andDo(canEvaluatePolicyHandler); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertTrue([result boolValue]); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FlutterError *error; + NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; + XCTAssertTrue([result boolValue]); + XCTAssertNil(error); } - (void)testDeviceSupportsBiometrics_withNoBiometricHardware { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { @@ -415,72 +423,51 @@ - (void)testDeviceSupportsBiometrics_withNoBiometricHardware { error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) .andDo(canEvaluatePolicyHandler); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertFalse([result boolValue]); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FlutterError *error; + NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; + XCTAssertFalse([result boolValue]); + XCTAssertNil(error); } -- (void)testGetEnrolledBiometrics_withFaceID { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; +- (void)testGetEnrolledBiometricsWithFaceID { id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeFaceID); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSArray class]]); - XCTAssertEqual([result count], 1); - XCTAssertEqualObjects(result[0], @"face"); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FlutterError *error; + NSArray *result = [plugin getEnrolledBiometricsWithError:&error]; + XCTAssertEqual([result count], 1); + XCTAssertEqual(result[0].value, FLAAuthBiometricFace); + XCTAssertNil(error); } -- (void)testGetEnrolledBiometrics_withTouchID { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; +- (void)testGetEnrolledBiometricsWithTouchID { id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeTouchID); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSArray class]]); - XCTAssertEqual([result count], 1); - XCTAssertEqualObjects(result[0], @"fingerprint"); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + FlutterError *error; + NSArray *result = [plugin getEnrolledBiometricsWithError:&error]; + XCTAssertEqual([result count], 1); + XCTAssertEqual(result[0].value, FLAAuthBiometricFingerprint); + XCTAssertNil(error); } -- (void)testGetEnrolledBiometrics_withoutEnrolledHardware { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; +- (void)testGetEnrolledBiometricsWithoutEnrolledHardware { id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { @@ -497,17 +484,32 @@ - (void)testGetEnrolledBiometrics_withoutEnrolledHardware { error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) .andDo(canEvaluatePolicyHandler); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" - arguments:@{}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSArray class]]); - XCTAssertEqual([result count], 0); - [expectation fulfill]; - }]; + FlutterError *error; + NSArray *result = [plugin getEnrolledBiometricsWithError:&error]; + XCTAssertEqual([result count], 0); + XCTAssertNil(error); +} - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +// TODO(stuartmorgan): Make this multiple tests when fixing +// https://github.com/flutter/flutter/issues/116179 +// Currently it just always returns true. +- (void)testIsDeviceSupported { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] initWithContexts:@[]]]; + + FlutterError *error; + NSNumber *result = [plugin isDeviceSupportedWithError:&error]; + XCTAssertTrue([result boolValue]); + XCTAssertNil(error); +} + +// Creates an FLAAuthStrings with placeholder values. +- (FLAAuthStrings *)createAuthStrings { + return [FLAAuthStrings makeWithReason:@"a reason" + lockOut:@"locked out" + goToSettingsButton:@"Go To Settings" + goToSettingsDescription:@"Settings" + cancelButton:@"Cancel" + localizedFallbackTitle:nil]; } @end diff --git a/packages/local_auth/local_auth_ios/example/pubspec.yaml b/packages/local_auth/local_auth_ios/example/pubspec.yaml index a7e79c233173..300f2ff967ea 100644 --- a/packages/local_auth/local_auth_ios/example/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the local_auth_ios plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.h b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.h index 1a1446fb27bd..d023ba3ed140 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.h +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.h @@ -4,5 +4,7 @@ #import -@interface FLTLocalAuthPlugin : NSObject +#import "messages.g.h" + +@interface FLTLocalAuthPlugin : NSObject @end diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m index 10c1e812fbcb..ea105f3943ce 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -1,200 +1,202 @@ // 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 "FLTLocalAuthPlugin.h" +#import "FLTLocalAuthPlugin_Test.h" + #import -#import "FLTLocalAuthPlugin.h" +typedef void (^FLAAuthCompletion)(FLAAuthResultDetails *_Nullable, FlutterError *_Nullable); -@interface FLTLocalAuthPlugin () -@property(nonatomic, copy, nullable) NSDictionary *lastCallArgs; -@property(nonatomic, nullable) FlutterResult lastResult; -// For unit tests to inject dummy LAContext instances that will be used when a new context would -// normally be created. Each call to createAuthContext will remove the current first element from -// the array. -- (void)setAuthContextOverrides:(NSArray *)authContexts; +/** + * A default context factory that wraps standard LAContext allocation. + */ +@interface FLADefaultAuthContextFactory : NSObject @end -@implementation FLTLocalAuthPlugin { - NSMutableArray *_authContextOverrides; +@implementation FLADefaultAuthContextFactory +- (LAContext *)createAuthContext { + return [[LAContext alloc] init]; } +@end -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/local_auth_ios" - binaryMessenger:[registrar messenger]]; - FLTLocalAuthPlugin *instance = [[FLTLocalAuthPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; - [registrar addApplicationDelegate:instance]; -} +#pragma mark - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([@"authenticate" isEqualToString:call.method]) { - bool isBiometricOnly = [call.arguments[@"biometricOnly"] boolValue]; - if (isBiometricOnly) { - [self authenticateWithBiometrics:call.arguments withFlutterResult:result]; - } else { - [self authenticate:call.arguments withFlutterResult:result]; - } - } else if ([@"getEnrolledBiometrics" isEqualToString:call.method]) { - [self getEnrolledBiometrics:result]; - } else if ([@"deviceSupportsBiometrics" isEqualToString:call.method]) { - [self deviceSupportsBiometrics:result]; - } else if ([@"isDeviceSupported" isEqualToString:call.method]) { - result(@YES); - } else { - result(FlutterMethodNotImplemented); +/** + * A data container for sticky auth state. + */ +@interface FLAStickyAuthState : NSObject +@property(nonatomic, strong, nonnull) FLAAuthOptions *options; +@property(nonatomic, strong, nonnull) FLAAuthStrings *strings; +@property(nonatomic, copy, nonnull) FLAAuthCompletion resultHandler; +- (instancetype)initWithOptions:(nonnull FLAAuthOptions *)options + strings:(nonnull FLAAuthStrings *)strings + resultHandler:(nonnull FLAAuthCompletion)resultHandler; +@end + +@implementation FLAStickyAuthState +- (instancetype)initWithOptions:(nonnull FLAAuthOptions *)options + strings:(nonnull FLAAuthStrings *)strings + resultHandler:(nonnull FLAAuthCompletion)resultHandler { + self = [super init]; + if (self) { + _options = options; + _strings = strings; + _resultHandler = resultHandler; } + return self; } +@end -#pragma mark Private Methods +#pragma mark - + +@interface FLTLocalAuthPlugin () +@property(nonatomic, strong, nullable) FLAStickyAuthState *lastCallState; +@property(nonatomic, strong) NSObject *authContextFactory; +@end + +@implementation FLTLocalAuthPlugin + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FLTLocalAuthPlugin *instance = [[FLTLocalAuthPlugin alloc] init]; + [registrar addApplicationDelegate:instance]; + FLALocalAuthApiSetup([registrar messenger], instance); +} -- (void)setAuthContextOverrides:(NSArray *)authContexts { - _authContextOverrides = [authContexts mutableCopy]; +- (instancetype)init { + return [self initWithContextFactory:[[FLADefaultAuthContextFactory alloc] init]]; } -- (LAContext *)createAuthContext { - if ([_authContextOverrides count] > 0) { - LAContext *context = [_authContextOverrides firstObject]; - [_authContextOverrides removeObjectAtIndex:0]; - return context; +- (instancetype)initWithContextFactory:(NSObject *)factory { + self = [super init]; + if (self) { + _authContextFactory = factory; } - return [[LAContext alloc] init]; + return self; } -- (void)alertMessage:(NSString *)message - firstButton:(NSString *)firstButton - flutterResult:(FlutterResult)result - additionalButton:(NSString *)secondButton { - UIAlertController *alert = - [UIAlertController alertControllerWithTitle:@"" - message:message - preferredStyle:UIAlertControllerStyleAlert]; +#pragma mark FLALocalAuthApi - UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:firstButton - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - result(@NO); - }]; +- (void)authenticateWithOptions:(nonnull FLAAuthOptions *)options + strings:(nonnull FLAAuthStrings *)strings + completion:(nonnull void (^)(FLAAuthResultDetails *_Nullable, + FlutterError *_Nullable))completion { + LAContext *context = [self.authContextFactory createAuthContext]; + NSError *authError = nil; + self.lastCallState = nil; + context.localizedFallbackTitle = strings.localizedFallbackTitle; - [alert addAction:defaultAction]; - if (secondButton != nil) { - UIAlertAction *additionalAction = [UIAlertAction - actionWithTitle:secondButton - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - if (UIApplicationOpenSettingsURLString != NULL) { - NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; - [[UIApplication sharedApplication] openURL:url - options:@{} - completionHandler:NULL]; - result(@NO); - } - }]; - [alert addAction:additionalAction]; + LAPolicy policy = options.biometricOnly.boolValue + ? LAPolicyDeviceOwnerAuthenticationWithBiometrics + : LAPolicyDeviceOwnerAuthentication; + if ([context canEvaluatePolicy:policy error:&authError]) { + [context evaluatePolicy:policy + localizedReason:strings.reason + reply:^(BOOL success, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self handleAuthReplyWithSuccess:success + error:error + options:options + strings:strings + completion:completion]; + }); + }]; + } else { + [self handleError:authError withOptions:options strings:strings completion:completion]; } - [[UIApplication sharedApplication].delegate.window.rootViewController presentViewController:alert - animated:YES - completion:nil]; } -- (void)deviceSupportsBiometrics:(FlutterResult)result { - LAContext *context = self.createAuthContext; +- (nullable NSNumber *)deviceCanSupportBiometricsWithError: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + LAContext *context = [self.authContextFactory createAuthContext]; NSError *authError = nil; // Check if authentication with biometrics is possible. if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) { if (authError == nil) { - result(@YES); - return; + return @YES; } } // If not, check if it is because no biometrics are enrolled (but still present). if (authError != nil) { if (authError.code == LAErrorBiometryNotEnrolled) { - result(@YES); - return; + return @YES; } } - result(@NO); + return @NO; } -- (void)getEnrolledBiometrics:(FlutterResult)result { - LAContext *context = self.createAuthContext; +- (nullable NSArray *)getEnrolledBiometricsWithError: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + LAContext *context = [self.authContextFactory createAuthContext]; NSError *authError = nil; - NSMutableArray *biometrics = [[NSMutableArray alloc] init]; + NSMutableArray *biometrics = [[NSMutableArray alloc] init]; if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) { if (authError == nil) { if (context.biometryType == LABiometryTypeFaceID) { - [biometrics addObject:@"face"]; + [biometrics addObject:[FLAAuthBiometricWrapper makeWithValue:FLAAuthBiometricFace]]; } else if (context.biometryType == LABiometryTypeTouchID) { - [biometrics addObject:@"fingerprint"]; + [biometrics addObject:[FLAAuthBiometricWrapper makeWithValue:FLAAuthBiometricFingerprint]]; } } } - result(biometrics); + return biometrics; } -- (void)authenticateWithBiometrics:(NSDictionary *)arguments - withFlutterResult:(FlutterResult)result { - LAContext *context = self.createAuthContext; - NSError *authError = nil; - self.lastCallArgs = nil; - self.lastResult = nil; - context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] - ? nil - : arguments[@"localizedFallbackTitle"]; - - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics - error:&authError]) { - [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics - localizedReason:arguments[@"localizedReason"] - reply:^(BOOL success, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self handleAuthReplyWithSuccess:success - error:error - flutterArguments:arguments - flutterResult:result]; - }); - }]; - } else { - [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; - } +- (nullable NSNumber *)isDeviceSupportedWithError: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + // TODO(stuartmorgan): Fix this to check for biometrics or passcode; see + // https://github.com/flutter/flutter/issues/116179 + return @YES; } -- (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { - LAContext *context = self.createAuthContext; - NSError *authError = nil; - _lastCallArgs = nil; - _lastResult = nil; - context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] - ? nil - : arguments[@"localizedFallbackTitle"]; +#pragma mark Private Methods - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { - [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication - localizedReason:arguments[@"localizedReason"] - reply:^(BOOL success, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self handleAuthReplyWithSuccess:success - error:error - flutterArguments:arguments - flutterResult:result]; - }); - }]; - } else { - [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; +- (void)showAlertWithMessage:(NSString *)message + dismissButtonTitle:(NSString *)dismissButtonTitle + openSettingsButtonTitle:(NSString *)openSettingsButtonTitle + completion:(FLAAuthCompletion)completion { + UIAlertController *alert = + [UIAlertController alertControllerWithTitle:@"" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:dismissButtonTitle + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [self handleSucceeded:NO + withCompletion:completion]; + }]; + + [alert addAction:defaultAction]; + if (openSettingsButtonTitle != nil) { + UIAlertAction *additionalAction = [UIAlertAction + actionWithTitle:openSettingsButtonTitle + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; + [[UIApplication sharedApplication] openURL:url + options:@{} + completionHandler:NULL]; + [self handleSucceeded:NO withCompletion:completion]; + }]; + [alert addAction:additionalAction]; } + [[UIApplication sharedApplication].delegate.window.rootViewController presentViewController:alert + animated:YES + completion:nil]; } - (void)handleAuthReplyWithSuccess:(BOOL)success error:(NSError *)error - flutterArguments:(NSDictionary *)arguments - flutterResult:(FlutterResult)result { + options:(FLAAuthOptions *)options + strings:(FLAAuthStrings *)strings + completion:(nonnull FLAAuthCompletion)completion { NSAssert([NSThread isMainThread], @"Response handling must be done on the main thread."); if (success) { - result(@YES); + [self handleSucceeded:YES withCompletion:completion]; } else { switch (error.code) { case LAErrorBiometryNotAvailable: @@ -203,54 +205,68 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success case LAErrorUserFallback: case LAErrorPasscodeNotSet: case LAErrorAuthenticationFailed: - [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; + [self handleError:error withOptions:options strings:strings completion:completion]; return; case LAErrorSystemCancel: - if ([arguments[@"stickyAuth"] boolValue]) { - self->_lastCallArgs = arguments; - self->_lastResult = result; + if ([options.sticky boolValue]) { + _lastCallState = [[FLAStickyAuthState alloc] initWithOptions:options + strings:strings + resultHandler:completion]; } else { - result(@NO); + [self handleSucceeded:NO withCompletion:completion]; } return; } - [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; + [self handleError:error withOptions:options strings:strings completion:completion]; } } -- (void)handleErrors:(NSError *)authError - flutterArguments:(NSDictionary *)arguments - withFlutterResult:(FlutterResult)result { - NSString *errorCode = @"NotAvailable"; +- (void)handleSucceeded:(BOOL)succeeded withCompletion:(nonnull FLAAuthCompletion)completion { + completion( + [FLAAuthResultDetails makeWithResult:(succeeded ? FLAAuthResultSuccess : FLAAuthResultFailure) + errorMessage:nil + errorDetails:nil], + nil); +} + +- (void)handleError:(NSError *)authError + withOptions:(FLAAuthOptions *)options + strings:(FLAAuthStrings *)strings + completion:(nonnull FLAAuthCompletion)completion { + FLAAuthResult result = FLAAuthResultErrorNotAvailable; switch (authError.code) { case LAErrorPasscodeNotSet: case LAErrorBiometryNotEnrolled: - if ([arguments[@"useErrorDialogs"] boolValue]) { - [self alertMessage:arguments[@"goToSettingDescriptionIOS"] - firstButton:arguments[@"okButton"] - flutterResult:result - additionalButton:arguments[@"goToSetting"]]; + if (options.useErrorDialogs.boolValue) { + [self showAlertWithMessage:strings.goToSettingsDescription + dismissButtonTitle:strings.cancelButton + openSettingsButtonTitle:strings.goToSettingsButton + completion:completion]; return; } - errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; + result = authError.code == LAErrorPasscodeNotSet ? FLAAuthResultErrorPasscodeNotSet + : FLAAuthResultErrorNotEnrolled; break; case LAErrorBiometryLockout: - [self alertMessage:arguments[@"lockOut"] - firstButton:arguments[@"okButton"] - flutterResult:result - additionalButton:nil]; + [self showAlertWithMessage:strings.lockOut + dismissButtonTitle:strings.cancelButton + openSettingsButtonTitle:nil + completion:completion]; return; } - result([FlutterError errorWithCode:errorCode - message:authError.localizedDescription - details:authError.domain]); + completion([FLAAuthResultDetails makeWithResult:result + errorMessage:authError.localizedDescription + errorDetails:authError.domain], + nil); } #pragma mark - AppDelegate - (void)applicationDidBecomeActive:(UIApplication *)application { - if (self.lastCallArgs != nil && self.lastResult != nil) { - [self authenticateWithBiometrics:_lastCallArgs withFlutterResult:self.lastResult]; + if (self.lastCallState != nil) { + [self authenticateWithOptions:_lastCallState.options + strings:_lastCallState.strings + completion:_lastCallState.resultHandler]; } } diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin_Test.h b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin_Test.h new file mode 100644 index 000000000000..c35322033f99 --- /dev/null +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin_Test.h @@ -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 +#import + +/** + * Protocol for a source of LAContext instances. Used to allow context injection in unit tests. + */ +@protocol FLAAuthContextFactory +- (LAContext *)createAuthContext; +@end + +@interface FLTLocalAuthPlugin () +/** + * Returns an instance that uses the given factory to create LAContexts. + */ +- (instancetype)initWithContextFactory:(NSObject *)factory + NS_DESIGNATED_INITIALIZER; +@end diff --git a/packages/local_auth/local_auth_ios/ios/Classes/messages.g.h b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.h new file mode 100644 index 000000000000..eda93cd93e6b --- /dev/null +++ b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.h @@ -0,0 +1,123 @@ +// 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 + +@protocol FlutterBinaryMessenger; +@protocol FlutterMessageCodec; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + +/// Possible outcomes of an authentication attempt. +typedef NS_ENUM(NSUInteger, FLAAuthResult) { + /// The user authenticated successfully. + FLAAuthResultSuccess = 0, + /// The user failed to successfully authenticate. + FLAAuthResultFailure = 1, + /// The authentication system was not available. + FLAAuthResultErrorNotAvailable = 2, + /// No biometrics are enrolled. + FLAAuthResultErrorNotEnrolled = 3, + /// No passcode is set. + FLAAuthResultErrorPasscodeNotSet = 4, +}; + +/// Pigeon equivalent of the subset of BiometricType used by iOS. +typedef NS_ENUM(NSUInteger, FLAAuthBiometric) { + FLAAuthBiometricFace = 0, + FLAAuthBiometricFingerprint = 1, +}; + +@class FLAAuthStrings; +@class FLAAuthOptions; +@class FLAAuthResultDetails; +@class FLAAuthBiometricWrapper; + +/// Pigeon version of IOSAuthMessages, plus the authorization reason. +/// +/// See auth_messages_ios.dart for details. +@interface FLAAuthStrings : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithReason:(NSString *)reason + lockOut:(NSString *)lockOut + goToSettingsButton:(NSString *)goToSettingsButton + goToSettingsDescription:(NSString *)goToSettingsDescription + cancelButton:(NSString *)cancelButton + localizedFallbackTitle:(nullable NSString *)localizedFallbackTitle; +@property(nonatomic, copy) NSString *reason; +@property(nonatomic, copy) NSString *lockOut; +@property(nonatomic, copy) NSString *goToSettingsButton; +@property(nonatomic, copy) NSString *goToSettingsDescription; +@property(nonatomic, copy) NSString *cancelButton; +@property(nonatomic, copy, nullable) NSString *localizedFallbackTitle; +@end + +@interface FLAAuthOptions : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithBiometricOnly:(NSNumber *)biometricOnly + sticky:(NSNumber *)sticky + useErrorDialogs:(NSNumber *)useErrorDialogs; +@property(nonatomic, strong) NSNumber *biometricOnly; +@property(nonatomic, strong) NSNumber *sticky; +@property(nonatomic, strong) NSNumber *useErrorDialogs; +@end + +@interface FLAAuthResultDetails : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithResult:(FLAAuthResult)result + errorMessage:(nullable NSString *)errorMessage + errorDetails:(nullable NSString *)errorDetails; +/// The result of authenticating. +@property(nonatomic, assign) FLAAuthResult result; +/// A system-provided error message, if any. +@property(nonatomic, copy, nullable) NSString *errorMessage; +/// System-provided error details, if any. +@property(nonatomic, copy, nullable) NSString *errorDetails; +@end + +@interface FLAAuthBiometricWrapper : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithValue:(FLAAuthBiometric)value; +@property(nonatomic, assign) FLAAuthBiometric value; +@end + +/// The codec used by FLALocalAuthApi. +NSObject *FLALocalAuthApiGetCodec(void); + +@protocol FLALocalAuthApi +/// Returns true if this device supports authentication. +/// +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)isDeviceSupportedWithError:(FlutterError *_Nullable *_Nonnull)error; +/// Returns true if this device can support biometric authentication, whether +/// any biometrics are enrolled or not. +/// +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)deviceCanSupportBiometricsWithError:(FlutterError *_Nullable *_Nonnull)error; +/// Returns the biometric types that are enrolled, and can thus be used +/// without additional setup. +/// +/// @return `nil` only when `error != nil`. +- (nullable NSArray *)getEnrolledBiometricsWithError: + (FlutterError *_Nullable *_Nonnull)error; +/// Attempts to authenticate the user with the provided [options], and using +/// [strings] for any UI. +- (void)authenticateWithOptions:(FLAAuthOptions *)options + strings:(FLAAuthStrings *)strings + completion:(void (^)(FLAAuthResultDetails *_Nullable, + FlutterError *_Nullable))completion; +@end + +extern void FLALocalAuthApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +NS_ASSUME_NONNULL_END diff --git a/packages/local_auth/local_auth_ios/ios/Classes/messages.g.m b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.m new file mode 100644 index 000000000000..550e7ada9e2f --- /dev/null +++ b/packages/local_auth/local_auth_ios/ios/Classes/messages.g.m @@ -0,0 +1,333 @@ +// 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 "messages.g.h" +#import + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSArray *wrapResult(id result, FlutterError *error) { + if (error) { + return @[ + error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null] + ]; + } + return @[ result ?: [NSNull null] ]; +} +static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + +@interface FLAAuthStrings () ++ (FLAAuthStrings *)fromList:(NSArray *)list; ++ (nullable FLAAuthStrings *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FLAAuthOptions () ++ (FLAAuthOptions *)fromList:(NSArray *)list; ++ (nullable FLAAuthOptions *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FLAAuthResultDetails () ++ (FLAAuthResultDetails *)fromList:(NSArray *)list; ++ (nullable FLAAuthResultDetails *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FLAAuthBiometricWrapper () ++ (FLAAuthBiometricWrapper *)fromList:(NSArray *)list; ++ (nullable FLAAuthBiometricWrapper *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@implementation FLAAuthStrings ++ (instancetype)makeWithReason:(NSString *)reason + lockOut:(NSString *)lockOut + goToSettingsButton:(NSString *)goToSettingsButton + goToSettingsDescription:(NSString *)goToSettingsDescription + cancelButton:(NSString *)cancelButton + localizedFallbackTitle:(nullable NSString *)localizedFallbackTitle { + FLAAuthStrings *pigeonResult = [[FLAAuthStrings alloc] init]; + pigeonResult.reason = reason; + pigeonResult.lockOut = lockOut; + pigeonResult.goToSettingsButton = goToSettingsButton; + pigeonResult.goToSettingsDescription = goToSettingsDescription; + pigeonResult.cancelButton = cancelButton; + pigeonResult.localizedFallbackTitle = localizedFallbackTitle; + return pigeonResult; +} ++ (FLAAuthStrings *)fromList:(NSArray *)list { + FLAAuthStrings *pigeonResult = [[FLAAuthStrings alloc] init]; + pigeonResult.reason = GetNullableObjectAtIndex(list, 0); + NSAssert(pigeonResult.reason != nil, @""); + pigeonResult.lockOut = GetNullableObjectAtIndex(list, 1); + NSAssert(pigeonResult.lockOut != nil, @""); + pigeonResult.goToSettingsButton = GetNullableObjectAtIndex(list, 2); + NSAssert(pigeonResult.goToSettingsButton != nil, @""); + pigeonResult.goToSettingsDescription = GetNullableObjectAtIndex(list, 3); + NSAssert(pigeonResult.goToSettingsDescription != nil, @""); + pigeonResult.cancelButton = GetNullableObjectAtIndex(list, 4); + NSAssert(pigeonResult.cancelButton != nil, @""); + pigeonResult.localizedFallbackTitle = GetNullableObjectAtIndex(list, 5); + return pigeonResult; +} ++ (nullable FLAAuthStrings *)nullableFromList:(NSArray *)list { + return (list) ? [FLAAuthStrings fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.reason ?: [NSNull null]), + (self.lockOut ?: [NSNull null]), + (self.goToSettingsButton ?: [NSNull null]), + (self.goToSettingsDescription ?: [NSNull null]), + (self.cancelButton ?: [NSNull null]), + (self.localizedFallbackTitle ?: [NSNull null]), + ]; +} +@end + +@implementation FLAAuthOptions ++ (instancetype)makeWithBiometricOnly:(NSNumber *)biometricOnly + sticky:(NSNumber *)sticky + useErrorDialogs:(NSNumber *)useErrorDialogs { + FLAAuthOptions *pigeonResult = [[FLAAuthOptions alloc] init]; + pigeonResult.biometricOnly = biometricOnly; + pigeonResult.sticky = sticky; + pigeonResult.useErrorDialogs = useErrorDialogs; + return pigeonResult; +} ++ (FLAAuthOptions *)fromList:(NSArray *)list { + FLAAuthOptions *pigeonResult = [[FLAAuthOptions alloc] init]; + pigeonResult.biometricOnly = GetNullableObjectAtIndex(list, 0); + NSAssert(pigeonResult.biometricOnly != nil, @""); + pigeonResult.sticky = GetNullableObjectAtIndex(list, 1); + NSAssert(pigeonResult.sticky != nil, @""); + pigeonResult.useErrorDialogs = GetNullableObjectAtIndex(list, 2); + NSAssert(pigeonResult.useErrorDialogs != nil, @""); + return pigeonResult; +} ++ (nullable FLAAuthOptions *)nullableFromList:(NSArray *)list { + return (list) ? [FLAAuthOptions fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.biometricOnly ?: [NSNull null]), + (self.sticky ?: [NSNull null]), + (self.useErrorDialogs ?: [NSNull null]), + ]; +} +@end + +@implementation FLAAuthResultDetails ++ (instancetype)makeWithResult:(FLAAuthResult)result + errorMessage:(nullable NSString *)errorMessage + errorDetails:(nullable NSString *)errorDetails { + FLAAuthResultDetails *pigeonResult = [[FLAAuthResultDetails alloc] init]; + pigeonResult.result = result; + pigeonResult.errorMessage = errorMessage; + pigeonResult.errorDetails = errorDetails; + return pigeonResult; +} ++ (FLAAuthResultDetails *)fromList:(NSArray *)list { + FLAAuthResultDetails *pigeonResult = [[FLAAuthResultDetails alloc] init]; + pigeonResult.result = [GetNullableObjectAtIndex(list, 0) integerValue]; + pigeonResult.errorMessage = GetNullableObjectAtIndex(list, 1); + pigeonResult.errorDetails = GetNullableObjectAtIndex(list, 2); + return pigeonResult; +} ++ (nullable FLAAuthResultDetails *)nullableFromList:(NSArray *)list { + return (list) ? [FLAAuthResultDetails fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.result), + (self.errorMessage ?: [NSNull null]), + (self.errorDetails ?: [NSNull null]), + ]; +} +@end + +@implementation FLAAuthBiometricWrapper ++ (instancetype)makeWithValue:(FLAAuthBiometric)value { + FLAAuthBiometricWrapper *pigeonResult = [[FLAAuthBiometricWrapper alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FLAAuthBiometricWrapper *)fromList:(NSArray *)list { + FLAAuthBiometricWrapper *pigeonResult = [[FLAAuthBiometricWrapper alloc] init]; + pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; + return pigeonResult; +} ++ (nullable FLAAuthBiometricWrapper *)nullableFromList:(NSArray *)list { + return (list) ? [FLAAuthBiometricWrapper fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.value), + ]; +} +@end + +@interface FLALocalAuthApiCodecReader : FlutterStandardReader +@end +@implementation FLALocalAuthApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FLAAuthBiometricWrapper fromList:[self readValue]]; + case 129: + return [FLAAuthOptions fromList:[self readValue]]; + case 130: + return [FLAAuthResultDetails fromList:[self readValue]]; + case 131: + return [FLAAuthStrings fromList:[self readValue]]; + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FLALocalAuthApiCodecWriter : FlutterStandardWriter +@end +@implementation FLALocalAuthApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FLAAuthBiometricWrapper class]]) { + [self writeByte:128]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FLAAuthOptions class]]) { + [self writeByte:129]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FLAAuthResultDetails class]]) { + [self writeByte:130]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FLAAuthStrings class]]) { + [self writeByte:131]; + [self writeValue:[value toList]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FLALocalAuthApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FLALocalAuthApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FLALocalAuthApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FLALocalAuthApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FLALocalAuthApiGetCodec(void) { + static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; + dispatch_once(&sPred, ^{ + FLALocalAuthApiCodecReaderWriter *readerWriter = + [[FLALocalAuthApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FLALocalAuthApiSetup(id binaryMessenger, + NSObject *api) { + /// Returns true if this device supports authentication. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.LocalAuthApi.isDeviceSupported" + binaryMessenger:binaryMessenger + codec:FLALocalAuthApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(isDeviceSupportedWithError:)], + @"FLALocalAuthApi api (%@) doesn't respond to @selector(isDeviceSupportedWithError:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSNumber *output = [api isDeviceSupportedWithError:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + /// Returns true if this device can support biometric authentication, whether + /// any biometrics are enrolled or not. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.LocalAuthApi.deviceCanSupportBiometrics" + binaryMessenger:binaryMessenger + codec:FLALocalAuthApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(deviceCanSupportBiometricsWithError:)], + @"FLALocalAuthApi api (%@) doesn't respond to " + @"@selector(deviceCanSupportBiometricsWithError:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSNumber *output = [api deviceCanSupportBiometricsWithError:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + /// Returns the biometric types that are enrolled, and can thus be used + /// without additional setup. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.LocalAuthApi.getEnrolledBiometrics" + binaryMessenger:binaryMessenger + codec:FLALocalAuthApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(getEnrolledBiometricsWithError:)], + @"FLALocalAuthApi api (%@) doesn't respond to @selector(getEnrolledBiometricsWithError:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSArray *output = [api getEnrolledBiometricsWithError:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + /// Attempts to authenticate the user with the provided [options], and using + /// [strings] for any UI. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.LocalAuthApi.authenticate" + binaryMessenger:binaryMessenger + codec:FLALocalAuthApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(authenticateWithOptions:strings:completion:)], + @"FLALocalAuthApi api (%@) doesn't respond to " + @"@selector(authenticateWithOptions:strings:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLAAuthOptions *arg_options = GetNullableObjectAtIndex(args, 0); + FLAAuthStrings *arg_strings = GetNullableObjectAtIndex(args, 1); + [api authenticateWithOptions:arg_options + strings:arg_strings + completion:^(FLAAuthResultDetails *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} diff --git a/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart b/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart index 217fd39d9901..439b1b308392 100644 --- a/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart +++ b/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart @@ -2,9 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/services.dart'; import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; +import 'src/messages.g.dart'; import 'types/auth_messages_ios.dart'; export 'package:local_auth_ios/types/auth_messages_ios.dart'; @@ -12,16 +14,20 @@ export 'package:local_auth_platform_interface/types/auth_messages.dart'; export 'package:local_auth_platform_interface/types/auth_options.dart'; export 'package:local_auth_platform_interface/types/biometric_type.dart'; -const MethodChannel _channel = - MethodChannel('plugins.flutter.io/local_auth_ios'); - /// The implementation of [LocalAuthPlatform] for iOS. class LocalAuthIOS extends LocalAuthPlatform { + /// Creates a new plugin implementation instance. + LocalAuthIOS({ + @visibleForTesting LocalAuthApi? api, + }) : _api = api ?? LocalAuthApi(); + /// Registers this class as the default instance of [LocalAuthPlatform]. static void registerWith() { LocalAuthPlatform.instance = LocalAuthIOS(); } + final LocalAuthApi _api; + @override Future authenticate({ required String localizedReason, @@ -29,58 +35,87 @@ class LocalAuthIOS extends LocalAuthPlatform { AuthenticationOptions options = const AuthenticationOptions(), }) async { assert(localizedReason.isNotEmpty); - final Map args = { - 'localizedReason': localizedReason, - 'useErrorDialogs': options.useErrorDialogs, - 'stickyAuth': options.stickyAuth, - 'sensitiveTransaction': options.sensitiveTransaction, - 'biometricOnly': options.biometricOnly, - }; - args.addAll(const IOSAuthMessages().args); - for (final AuthMessages messages in authMessages) { - if (messages is IOSAuthMessages) { - args.addAll(messages.args); - } + final AuthResultDetails resultDetails = await _api.authenticate( + AuthOptions( + biometricOnly: options.biometricOnly, + sticky: options.stickyAuth, + useErrorDialogs: options.useErrorDialogs), + _pigeonStringsFromAuthMessages(localizedReason, authMessages)); + // TODO(stuartmorgan): Replace this with structured errors, coordinated + // across all platform implementations, per + // https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#platform-exception-handling + // The PlatformExceptions thrown here are for compatibiilty with the + // previous Objective-C implementation. + switch (resultDetails.result) { + case AuthResult.success: + return true; + case AuthResult.failure: + return false; + case AuthResult.errorNotAvailable: + throw PlatformException( + code: 'NotAvailable', + message: resultDetails.errorMessage, + details: resultDetails.errorDetails); + case AuthResult.errorNotEnrolled: + throw PlatformException( + code: 'NotEnrolled', + message: resultDetails.errorMessage, + details: resultDetails.errorDetails); + case AuthResult.errorPasscodeNotSet: + throw PlatformException( + code: 'PasscodeNotSet', + message: resultDetails.errorMessage, + details: resultDetails.errorDetails); } - return (await _channel.invokeMethod('authenticate', args)) ?? false; } @override Future deviceSupportsBiometrics() async { - return (await _channel.invokeMethod('deviceSupportsBiometrics')) ?? - false; + return _api.deviceCanSupportBiometrics(); } @override Future> getEnrolledBiometrics() async { - final List result = (await _channel.invokeListMethod( - 'getEnrolledBiometrics', - )) ?? - []; - final List biometrics = []; - for (final String value in result) { - switch (value) { - case 'face': - biometrics.add(BiometricType.face); - break; - case 'fingerprint': - biometrics.add(BiometricType.fingerprint); - break; - case 'iris': - biometrics.add(BiometricType.iris); - break; + final List result = + await _api.getEnrolledBiometrics(); + return result + .cast() + .map((AuthBiometricWrapper entry) { + switch (entry.value) { + case AuthBiometric.face: + return BiometricType.face; + case AuthBiometric.fingerprint: + return BiometricType.fingerprint; } - } - return biometrics; + }).toList(); } @override - Future isDeviceSupported() async => - (await _channel.invokeMethod('isDeviceSupported')) ?? false; + Future isDeviceSupported() async => _api.isDeviceSupported(); /// Always returns false as this method is not supported on iOS. @override - Future stopAuthentication() async { - return false; + Future stopAuthentication() async => false; + + AuthStrings _pigeonStringsFromAuthMessages( + String localizedReason, Iterable messagesList) { + IOSAuthMessages? messages; + for (final AuthMessages entry in messagesList) { + if (entry is IOSAuthMessages) { + messages = entry; + break; + } + } + return AuthStrings( + reason: localizedReason, + lockOut: messages?.lockOut ?? iOSLockOut, + goToSettingsButton: messages?.goToSettingsButton ?? goToSettings, + goToSettingsDescription: + messages?.goToSettingsDescription ?? iOSGoToSettingsDescription, + // TODO(stuartmorgan): The default's name is confusing here for legacy + // reasons; this should be fixed as part of some future breaking change. + cancelButton: messages?.cancelButton ?? iOSOkButton, + localizedFallbackTitle: messages?.localizedFallbackTitle, + ); } } diff --git a/packages/local_auth/local_auth_ios/lib/src/messages.g.dart b/packages/local_auth/local_auth_ios/lib/src/messages.g.dart new file mode 100644 index 000000000000..48ab4e53232f --- /dev/null +++ b/packages/local_auth/local_auth_ios/lib/src/messages.g.dart @@ -0,0 +1,333 @@ +// 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'; + +/// Possible outcomes of an authentication attempt. +enum AuthResult { + /// The user authenticated successfully. + success, + + /// The user failed to successfully authenticate. + failure, + + /// The authentication system was not available. + errorNotAvailable, + + /// No biometrics are enrolled. + errorNotEnrolled, + + /// No passcode is set. + errorPasscodeNotSet, +} + +/// Pigeon equivalent of the subset of BiometricType used by iOS. +enum AuthBiometric { + face, + fingerprint, +} + +/// Pigeon version of IOSAuthMessages, plus the authorization reason. +/// +/// See auth_messages_ios.dart for details. +class AuthStrings { + AuthStrings({ + required this.reason, + required this.lockOut, + required this.goToSettingsButton, + required this.goToSettingsDescription, + required this.cancelButton, + this.localizedFallbackTitle, + }); + + String reason; + + String lockOut; + + String goToSettingsButton; + + String goToSettingsDescription; + + String cancelButton; + + String? localizedFallbackTitle; + + Object encode() { + return [ + reason, + lockOut, + goToSettingsButton, + goToSettingsDescription, + cancelButton, + localizedFallbackTitle, + ]; + } + + static AuthStrings decode(Object result) { + result as List; + return AuthStrings( + reason: result[0]! as String, + lockOut: result[1]! as String, + goToSettingsButton: result[2]! as String, + goToSettingsDescription: result[3]! as String, + cancelButton: result[4]! as String, + localizedFallbackTitle: result[5] as String?, + ); + } +} + +class AuthOptions { + AuthOptions({ + required this.biometricOnly, + required this.sticky, + required this.useErrorDialogs, + }); + + bool biometricOnly; + + bool sticky; + + bool useErrorDialogs; + + Object encode() { + return [ + biometricOnly, + sticky, + useErrorDialogs, + ]; + } + + static AuthOptions decode(Object result) { + result as List; + return AuthOptions( + biometricOnly: result[0]! as bool, + sticky: result[1]! as bool, + useErrorDialogs: result[2]! as bool, + ); + } +} + +class AuthResultDetails { + AuthResultDetails({ + required this.result, + this.errorMessage, + this.errorDetails, + }); + + /// The result of authenticating. + AuthResult result; + + /// A system-provided error message, if any. + String? errorMessage; + + /// System-provided error details, if any. + String? errorDetails; + + Object encode() { + return [ + result.index, + errorMessage, + errorDetails, + ]; + } + + static AuthResultDetails decode(Object result) { + result as List; + return AuthResultDetails( + result: AuthResult.values[result[0]! as int], + errorMessage: result[1] as String?, + errorDetails: result[2] as String?, + ); + } +} + +class AuthBiometricWrapper { + AuthBiometricWrapper({ + required this.value, + }); + + AuthBiometric value; + + Object encode() { + return [ + value.index, + ]; + } + + static AuthBiometricWrapper decode(Object result) { + result as List; + return AuthBiometricWrapper( + value: AuthBiometric.values[result[0]! as int], + ); + } +} + +class _LocalAuthApiCodec extends StandardMessageCodec { + const _LocalAuthApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is AuthBiometricWrapper) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is AuthOptions) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is AuthResultDetails) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is AuthStrings) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return AuthBiometricWrapper.decode(readValue(buffer)!); + case 129: + return AuthOptions.decode(readValue(buffer)!); + case 130: + return AuthResultDetails.decode(readValue(buffer)!); + case 131: + return AuthStrings.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class LocalAuthApi { + /// Constructor for [LocalAuthApi]. 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. + LocalAuthApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _LocalAuthApiCodec(); + + /// Returns true if this device supports authentication. + Future isDeviceSupported() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LocalAuthApi.isDeviceSupported', 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 bool?)!; + } + } + + /// Returns true if this device can support biometric authentication, whether + /// any biometrics are enrolled or not. + Future deviceCanSupportBiometrics() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LocalAuthApi.deviceCanSupportBiometrics', 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 bool?)!; + } + } + + /// Returns the biometric types that are enrolled, and can thus be used + /// without additional setup. + Future> getEnrolledBiometrics() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LocalAuthApi.getEnrolledBiometrics', 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 List?)!.cast(); + } + } + + /// Attempts to authenticate the user with the provided [options], and using + /// [strings] for any UI. + Future authenticate( + AuthOptions arg_options, AuthStrings arg_strings) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LocalAuthApi.authenticate', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_options, arg_strings]) 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 AuthResultDetails?)!; + } + } +} diff --git a/packages/local_auth/local_auth_ios/pigeons/copyright.txt b/packages/local_auth/local_auth_ios/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/local_auth/local_auth_ios/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/local_auth/local_auth_ios/pigeons/messages.dart b/packages/local_auth/local_auth_ios/pigeons/messages.dart new file mode 100644 index 000000000000..b230597684fc --- /dev/null +++ b/packages/local_auth/local_auth_ios/pigeons/messages.dart @@ -0,0 +1,113 @@ +// 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', + objcHeaderOut: 'ios/Classes/messages.g.h', + objcSourceOut: 'ios/Classes/messages.g.m', + objcOptions: ObjcOptions( + prefix: 'FLA', + ), + copyrightHeader: 'pigeons/copyright.txt', +)) + +/// Pigeon version of IOSAuthMessages, plus the authorization reason. +/// +/// See auth_messages_ios.dart for details. +class AuthStrings { + /// Constructs a new instance. + const AuthStrings({ + required this.reason, + required this.lockOut, + required this.goToSettingsButton, + required this.goToSettingsDescription, + required this.cancelButton, + required this.localizedFallbackTitle, + }); + + final String reason; + final String lockOut; + final String goToSettingsButton; + final String goToSettingsDescription; + final String cancelButton; + final String? localizedFallbackTitle; +} + +/// Possible outcomes of an authentication attempt. +enum AuthResult { + /// The user authenticated successfully. + success, + + /// The user failed to successfully authenticate. + failure, + + /// The authentication system was not available. + errorNotAvailable, + + /// No biometrics are enrolled. + errorNotEnrolled, + + /// No passcode is set. + errorPasscodeNotSet, +} + +class AuthOptions { + AuthOptions( + {required this.biometricOnly, + required this.sticky, + required this.useErrorDialogs}); + final bool biometricOnly; + final bool sticky; + final bool useErrorDialogs; +} + +class AuthResultDetails { + AuthResultDetails( + {required this.result, this.errorMessage, this.errorDetails}); + + /// The result of authenticating. + final AuthResult result; + + /// A system-provided error message, if any. + final String? errorMessage; + + /// System-provided error details, if any. + // TODO(stuartmorgan): Remove this when standardizing errors plugin-wide in + // a breaking change. This is here only to preserve the existing error format + // exactly for compatibility, in case clients were checking PlatformException + // details. + final String? errorDetails; +} + +/// Pigeon equivalent of the subset of BiometricType used by iOS. +enum AuthBiometric { face, fingerprint } + +// TODO(stuartmorgan): Remove this when +// https://github.com/flutter/flutter/issues/87307 is implemented. +class AuthBiometricWrapper { + AuthBiometricWrapper({required this.value}); + final AuthBiometric value; +} + +@HostApi() +abstract class LocalAuthApi { + /// Returns true if this device supports authentication. + bool isDeviceSupported(); + + /// Returns true if this device can support biometric authentication, whether + /// any biometrics are enrolled or not. + bool deviceCanSupportBiometrics(); + + /// Returns the biometric types that are enrolled, and can thus be used + /// without additional setup. + List getEnrolledBiometrics(); + + /// Attempts to authenticate the user with the provided [options], and using + /// [strings] for any UI. + @async + @ObjCSelector('authenticateWithOptions:strings:') + AuthResultDetails authenticate(AuthOptions options, AuthStrings strings); +} diff --git a/packages/local_auth/local_auth_ios/pubspec.yaml b/packages/local_auth/local_auth_ios/pubspec.yaml index 544e820c082e..0fa01c1470d5 100644 --- a/packages/local_auth/local_auth_ios/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_ios description: iOS implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.1.1 +version: 1.1.3 environment: sdk: ">=2.18.0 <4.0.0" @@ -23,5 +23,8 @@ dependencies: local_auth_platform_interface: ^1.0.1 dev_dependencies: + build_runner: ^2.3.3 flutter_test: sdk: flutter + mockito: 5.4.0 + pigeon: ^9.2.4 diff --git a/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart b/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart new file mode 100644 index 000000000000..3edfcd7899e2 --- /dev/null +++ b/packages/local_auth/local_auth_ios/test/local_auth_ios_test.dart @@ -0,0 +1,330 @@ +// 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/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:local_auth_ios/local_auth_ios.dart'; +import 'package:local_auth_ios/src/messages.g.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'local_auth_ios_test.mocks.dart'; + +@GenerateMocks([LocalAuthApi]) +void main() { + late MockLocalAuthApi api; + late LocalAuthIOS plugin; + + setUp(() { + api = MockLocalAuthApi(); + plugin = LocalAuthIOS(api: api); + }); + + test('registers instance', () { + LocalAuthIOS.registerWith(); + expect(LocalAuthPlatform.instance, isA()); + }); + + group('deviceSupportsBiometrics', () { + test('handles true', () async { + when(api.deviceCanSupportBiometrics()).thenAnswer((_) async => true); + expect(await plugin.deviceSupportsBiometrics(), true); + }); + + test('handles false', () async { + when(api.deviceCanSupportBiometrics()).thenAnswer((_) async => false); + expect(await plugin.deviceSupportsBiometrics(), false); + }); + }); + + group('isDeviceSupported', () { + test('handles true', () async { + when(api.isDeviceSupported()).thenAnswer((_) async => true); + expect(await plugin.isDeviceSupported(), true); + }); + + test('handles false', () async { + when(api.isDeviceSupported()).thenAnswer((_) async => false); + expect(await plugin.isDeviceSupported(), false); + }); + }); + + group('stopAuthentication', () { + test('always returns false', () async { + expect(await plugin.stopAuthentication(), false); + }); + }); + + group('getEnrolledBiometrics', () { + test('translates values', () async { + when(api.getEnrolledBiometrics()) + .thenAnswer((_) async => [ + AuthBiometricWrapper(value: AuthBiometric.face), + AuthBiometricWrapper(value: AuthBiometric.fingerprint), + ]); + + final List result = await plugin.getEnrolledBiometrics(); + + expect(result, [ + BiometricType.face, + BiometricType.fingerprint, + ]); + }); + + test('handles empty', () async { + when(api.getEnrolledBiometrics()) + .thenAnswer((_) async => []); + + final List result = await plugin.getEnrolledBiometrics(); + + expect(result, []); + }); + }); + + group('authenticate', () { + group('strings', () { + test('passes default values when nothing is provided', () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(result: AuthResult.success)); + + const String reason = 'test reason'; + await plugin.authenticate( + localizedReason: reason, authMessages: []); + + final VerificationResult result = + verify(api.authenticate(any, captureAny)); + final AuthStrings strings = result.captured[0] as AuthStrings; + expect(strings.reason, reason); + // These should all be the default values from + // auth_messages_ios.dart + expect(strings.lockOut, iOSLockOut); + expect(strings.goToSettingsButton, goToSettings); + expect(strings.goToSettingsDescription, iOSGoToSettingsDescription); + expect(strings.cancelButton, iOSOkButton); + expect(strings.localizedFallbackTitle, null); + }); + + test('passes default values when only other platform values are provided', + () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(result: AuthResult.success)); + + const String reason = 'test reason'; + await plugin.authenticate( + localizedReason: reason, + authMessages: [AnotherPlatformAuthMessages()]); + + final VerificationResult result = + verify(api.authenticate(any, captureAny)); + final AuthStrings strings = result.captured[0] as AuthStrings; + expect(strings.reason, reason); + // These should all be the default values from + // auth_messages_ios.dart + expect(strings.lockOut, iOSLockOut); + expect(strings.goToSettingsButton, goToSettings); + expect(strings.goToSettingsDescription, iOSGoToSettingsDescription); + expect(strings.cancelButton, iOSOkButton); + expect(strings.localizedFallbackTitle, null); + }); + + test('passes all non-default values correctly', () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(result: AuthResult.success)); + + // These are arbitrary values; all that matters is that: + // - they are different from the defaults, and + // - they are different from each other. + const String reason = 'A'; + const String lockOut = 'B'; + const String goToSettingsButton = 'C'; + const String gotToSettingsDescription = 'D'; + const String cancel = 'E'; + const String localizedFallbackTitle = 'F'; + await plugin + .authenticate(localizedReason: reason, authMessages: [ + const IOSAuthMessages( + lockOut: lockOut, + goToSettingsButton: goToSettingsButton, + goToSettingsDescription: gotToSettingsDescription, + cancelButton: cancel, + localizedFallbackTitle: localizedFallbackTitle, + ), + AnotherPlatformAuthMessages(), + ]); + + final VerificationResult result = + verify(api.authenticate(any, captureAny)); + final AuthStrings strings = result.captured[0] as AuthStrings; + expect(strings.reason, reason); + expect(strings.lockOut, lockOut); + expect(strings.goToSettingsButton, goToSettingsButton); + expect(strings.goToSettingsDescription, gotToSettingsDescription); + expect(strings.cancelButton, cancel); + expect(strings.localizedFallbackTitle, localizedFallbackTitle); + }); + + test('passes provided messages with default fallbacks', () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(result: AuthResult.success)); + + // These are arbitrary values; all that matters is that: + // - they are different from the defaults, and + // - they are different from each other. + const String reason = 'A'; + const String lockOut = 'B'; + const String localizedFallbackTitle = 'C'; + const String cancel = 'D'; + await plugin + .authenticate(localizedReason: reason, authMessages: [ + const IOSAuthMessages( + lockOut: lockOut, + localizedFallbackTitle: localizedFallbackTitle, + cancelButton: cancel, + ), + ]); + + final VerificationResult result = + verify(api.authenticate(any, captureAny)); + final AuthStrings strings = result.captured[0] as AuthStrings; + expect(strings.reason, reason); + // These should all be the provided values. + expect(strings.lockOut, lockOut); + expect(strings.localizedFallbackTitle, localizedFallbackTitle); + expect(strings.cancelButton, cancel); + // These were not set, so should all be the default values from + // auth_messages_ios.dart + expect(strings.goToSettingsButton, goToSettings); + expect(strings.goToSettingsDescription, iOSGoToSettingsDescription); + }); + }); + + group('options', () { + test('passes default values', () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(result: AuthResult.success)); + + await plugin.authenticate( + localizedReason: 'reason', authMessages: []); + + final VerificationResult result = + verify(api.authenticate(captureAny, any)); + final AuthOptions options = result.captured[0] as AuthOptions; + expect(options.biometricOnly, false); + expect(options.sticky, false); + expect(options.useErrorDialogs, true); + }); + + test('passes provided non-default values', () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(result: AuthResult.success)); + + await plugin.authenticate( + localizedReason: 'reason', + authMessages: [], + options: const AuthenticationOptions( + biometricOnly: true, + stickyAuth: true, + useErrorDialogs: false, + )); + + final VerificationResult result = + verify(api.authenticate(captureAny, any)); + final AuthOptions options = result.captured[0] as AuthOptions; + expect(options.biometricOnly, true); + expect(options.sticky, true); + expect(options.useErrorDialogs, false); + }); + }); + + group('return values', () { + test('handles success', () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(result: AuthResult.success)); + + final bool result = await plugin.authenticate( + localizedReason: 'reason', authMessages: []); + + expect(result, true); + }); + + test('handles failure', () async { + when(api.authenticate(any, any)).thenAnswer( + (_) async => AuthResultDetails(result: AuthResult.failure)); + + final bool result = await plugin.authenticate( + localizedReason: 'reason', authMessages: []); + + expect(result, false); + }); + + test('converts errorNotAvailable to legacy PlatformException', () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer((_) async => + AuthResultDetails( + result: AuthResult.errorNotAvailable, + errorMessage: errorMessage, + errorDetails: errorDetails)); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', authMessages: []), + throwsA(isA() + .having((PlatformException e) => e.code, 'code', 'NotAvailable') + .having( + (PlatformException e) => e.message, 'message', errorMessage) + .having((PlatformException e) => e.details, 'details', + errorDetails))); + }); + + test('converts errorNotEnrolled to legacy PlatformException', () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer((_) async => + AuthResultDetails( + result: AuthResult.errorNotEnrolled, + errorMessage: errorMessage, + errorDetails: errorDetails)); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', authMessages: []), + throwsA(isA() + .having((PlatformException e) => e.code, 'code', 'NotEnrolled') + .having( + (PlatformException e) => e.message, 'message', errorMessage) + .having((PlatformException e) => e.details, 'details', + errorDetails))); + }); + + test('converts errorPasscodeNotSet to legacy PlatformException', + () async { + const String errorMessage = 'a message'; + const String errorDetails = 'some details'; + when(api.authenticate(any, any)).thenAnswer((_) async => + AuthResultDetails( + result: AuthResult.errorPasscodeNotSet, + errorMessage: errorMessage, + errorDetails: errorDetails)); + + expect( + () async => plugin.authenticate( + localizedReason: 'reason', authMessages: []), + throwsA(isA() + .having( + (PlatformException e) => e.code, 'code', 'PasscodeNotSet') + .having( + (PlatformException e) => e.message, 'message', errorMessage) + .having((PlatformException e) => e.details, 'details', + errorDetails))); + }); + }); + }); +} + +class AnotherPlatformAuthMessages extends AuthMessages { + @override + Map get args => throw UnimplementedError(); +} diff --git a/packages/local_auth/local_auth_ios/test/local_auth_ios_test.mocks.dart b/packages/local_auth/local_auth_ios/test/local_auth_ios_test.mocks.dart new file mode 100644 index 000000000000..97e0a9dba40d --- /dev/null +++ b/packages/local_auth/local_auth_ios/test/local_auth_ios_test.mocks.dart @@ -0,0 +1,92 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in local_auth_ios/test/local_auth_ios_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:local_auth_ios/src/messages.g.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// 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 + +class _FakeAuthResultDetails_0 extends _i1.SmartFake + implements _i2.AuthResultDetails { + _FakeAuthResultDetails_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [LocalAuthApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLocalAuthApi extends _i1.Mock implements _i2.LocalAuthApi { + MockLocalAuthApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future isDeviceSupported() => (super.noSuchMethod( + Invocation.method( + #isDeviceSupported, + [], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); + @override + _i3.Future deviceCanSupportBiometrics() => (super.noSuchMethod( + Invocation.method( + #deviceCanSupportBiometrics, + [], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); + @override + _i3.Future> getEnrolledBiometrics() => + (super.noSuchMethod( + Invocation.method( + #getEnrolledBiometrics, + [], + ), + returnValue: _i3.Future>.value( + <_i2.AuthBiometricWrapper?>[]), + ) as _i3.Future>); + @override + _i3.Future<_i2.AuthResultDetails> authenticate( + _i2.AuthOptions? arg_options, + _i2.AuthStrings? arg_strings, + ) => + (super.noSuchMethod( + Invocation.method( + #authenticate, + [ + arg_options, + arg_strings, + ], + ), + returnValue: + _i3.Future<_i2.AuthResultDetails>.value(_FakeAuthResultDetails_0( + this, + Invocation.method( + #authenticate, + [ + arg_options, + arg_strings, + ], + ), + )), + ) as _i3.Future<_i2.AuthResultDetails>); +} diff --git a/packages/local_auth/local_auth_ios/test/local_auth_test.dart b/packages/local_auth/local_auth_ios/test/local_auth_test.dart deleted file mode 100644 index 0d7f56d5da90..000000000000 --- a/packages/local_auth/local_auth_ios/test/local_auth_test.dart +++ /dev/null @@ -1,191 +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/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:local_auth_ios/local_auth_ios.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('LocalAuth', () { - const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/local_auth_ios', - ); - - final List log = []; - late LocalAuthIOS localAuthentication; - - setUp(() { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) { - log.add(methodCall); - switch (methodCall.method) { - case 'getEnrolledBiometrics': - return Future>.value( - ['face', 'fingerprint', 'iris', 'undefined']); - default: - return Future.value(true); - } - }); - localAuthentication = LocalAuthIOS(); - log.clear(); - }); - - test('deviceSupportsBiometrics calls platform', () async { - final bool result = await localAuthentication.deviceSupportsBiometrics(); - - expect( - log, - [ - isMethodCall('deviceSupportsBiometrics', arguments: null), - ], - ); - expect(result, true); - }); - - test('getEnrolledBiometrics calls platform', () async { - final List result = - await localAuthentication.getEnrolledBiometrics(); - - expect( - log, - [ - isMethodCall('getEnrolledBiometrics', arguments: null), - ], - ); - expect(result, [ - BiometricType.face, - BiometricType.fingerprint, - BiometricType.iris - ]); - }); - - test('isDeviceSupported calls platform', () async { - await localAuthentication.isDeviceSupported(); - - expect( - log, - [ - isMethodCall('isDeviceSupported', arguments: null), - ], - ); - }); - - test('stopAuthentication returns false', () async { - final bool result = await localAuthentication.stopAuthentication(); - expect(result, false); - }); - - group('With device auth fail over', () { - test('authenticate with no args.', () async { - await localAuthentication.authenticate( - authMessages: [const IOSAuthMessages()], - localizedReason: 'Needs secure', - options: const AuthenticationOptions(biometricOnly: true), - ); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': true, - }..addAll(const IOSAuthMessages().args)), - ], - ); - }); - - test('authenticate with no localizedReason.', () async { - await expectLater( - localAuthentication.authenticate( - authMessages: [const IOSAuthMessages()], - localizedReason: '', - options: const AuthenticationOptions(biometricOnly: true), - ), - throwsAssertionError, - ); - }); - }); - - group('With biometrics only', () { - test('authenticate with no args.', () async { - await localAuthentication.authenticate( - authMessages: [const IOSAuthMessages()], - localizedReason: 'Needs secure', - ); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': false, - }..addAll(const IOSAuthMessages().args)), - ], - ); - }); - - test('authenticate with `localizedFallbackTitle`', () async { - await localAuthentication.authenticate( - authMessages: [ - const IOSAuthMessages(localizedFallbackTitle: 'Enter PIN'), - ], - localizedReason: 'Needs secure', - ); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': false, - 'localizedFallbackTitle': 'Enter PIN', - }..addAll(const IOSAuthMessages().args)), - ], - ); - }); - - test('authenticate with no sensitive transaction.', () async { - await localAuthentication.authenticate( - authMessages: [const IOSAuthMessages()], - localizedReason: 'Insecure', - options: const AuthenticationOptions( - sensitiveTransaction: false, - useErrorDialogs: false, - ), - ); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Insecure', - 'useErrorDialogs': false, - 'stickyAuth': false, - 'sensitiveTransaction': false, - 'biometricOnly': false, - }..addAll(const IOSAuthMessages().args)), - ], - ); - }); - }); - }); -} - -/// 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 -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; diff --git a/packages/local_auth/local_auth_windows/CHANGELOG.md b/packages/local_auth/local_auth_windows/CHANGELOG.md index 5b44b321fd91..d184e5509a72 100644 --- a/packages/local_auth/local_auth_windows/CHANGELOG.md +++ b/packages/local_auth/local_auth_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 1.0.8 * Sets a cmake_policy compatibility version to fix build warnings. diff --git a/packages/local_auth/local_auth_windows/example/pubspec.yaml b/packages/local_auth/local_auth_windows/example/pubspec.yaml index 65be8041c033..2b31020e0753 100644 --- a/packages/local_auth/local_auth_windows/example/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the local_auth_windows plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_windows/pubspec.yaml b/packages/local_auth/local_auth_windows/pubspec.yaml index 01a9bdab1ee5..f0b97d3a1609 100644 --- a/packages/local_auth/local_auth_windows/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 1.0.8 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/metrics_center/CHANGELOG.md b/packages/metrics_center/CHANGELOG.md index ca2168267ab5..ba74ca19a830 100644 --- a/packages/metrics_center/CHANGELOG.md +++ b/packages/metrics_center/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 1.0.8 +* Removes obsolete null checks on non-nullable values. * Updates minimum Flutter version to 3.3. ## 1.0.7 diff --git a/packages/metrics_center/lib/src/gcs_lock.dart b/packages/metrics_center/lib/src/gcs_lock.dart index 5e6b2dfdca77..6f89fc83df53 100644 --- a/packages/metrics_center/lib/src/gcs_lock.dart +++ b/packages/metrics_center/lib/src/gcs_lock.dart @@ -12,9 +12,7 @@ class GcsLock { /// Create a lock with an authenticated client and a GCS bucket name. /// /// The client is used to communicate with Google Cloud Storage APIs. - GcsLock(this._client, this._bucketName) - : assert(_client != null), - assert(_bucketName != null) { + GcsLock(this._client, this._bucketName) { _api = StorageApi(_client); } diff --git a/packages/metrics_center/lib/src/skiaperf.dart b/packages/metrics_center/lib/src/skiaperf.dart index 1e30e635588d..45a00f34f502 100644 --- a/packages/metrics_center/lib/src/skiaperf.dart +++ b/packages/metrics_center/lib/src/skiaperf.dart @@ -201,7 +201,7 @@ class SkiaPerfPoint extends MetricPoint { class SkiaPerfGcsAdaptor { /// Construct the adaptor given the associated GCS bucket where the data is /// read from and written to. - SkiaPerfGcsAdaptor(this._gcsBucket) : assert(_gcsBucket != null); + SkiaPerfGcsAdaptor(this._gcsBucket); /// Used by Skia to differentiate json file format versions. static const int version = 1; @@ -285,7 +285,6 @@ class SkiaPerfGcsAdaptor { final String firstGcsNameComponent = objectName.split('/')[0]; _populateGcsNameToGithubRepoMapIfNeeded(); final String githubRepo = _gcsNameToGithubRepo[firstGcsNameComponent]!; - assert(githubRepo != null); final String? gitHash = decodedJson[kSkiaPerfGitHashKey] as String?; final Map results = @@ -403,11 +402,9 @@ class SkiaPerfDestination extends MetricDestination { >>{}; for (final SkiaPerfPoint p in points.map((MetricPoint x) => SkiaPerfPoint.fromPoint(x))) { - if (p != null) { - pointMap[p.githubRepo] ??= >{}; - pointMap[p.githubRepo]![p.gitHash] ??= {}; - pointMap[p.githubRepo]![p.gitHash]![p.id] = p; - } + pointMap[p.githubRepo] ??= >{}; + pointMap[p.githubRepo]![p.gitHash] ??= {}; + pointMap[p.githubRepo]![p.gitHash]![p.id] = p; } // All created locks must be released before returning diff --git a/packages/metrics_center/pubspec.yaml b/packages/metrics_center/pubspec.yaml index 289810749cee..488efd4a1a6e 100644 --- a/packages/metrics_center/pubspec.yaml +++ b/packages/metrics_center/pubspec.yaml @@ -1,5 +1,5 @@ name: metrics_center -version: 1.0.7 +version: 1.0.8 description: Support multiple performance metrics sources/formats and destinations. repository: https://github.com/flutter/packages/tree/main/packages/metrics_center diff --git a/packages/multicast_dns/CHANGELOG.md b/packages/multicast_dns/CHANGELOG.md index 55ee390df5d2..7e8642cd7e81 100644 --- a/packages/multicast_dns/CHANGELOG.md +++ b/packages/multicast_dns/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 0.3.2+3 * Removes use of `runtimeType.toString()`. diff --git a/packages/multicast_dns/pubspec.yaml b/packages/multicast_dns/pubspec.yaml index c50e0096a008..d0fc19ec3be6 100644 --- a/packages/multicast_dns/pubspec.yaml +++ b/packages/multicast_dns/pubspec.yaml @@ -5,7 +5,7 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 0.3.2+3 environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=2.18.0 <4.0.0" dependencies: meta: ^1.3.0 diff --git a/packages/palette_generator/CHANGELOG.md b/packages/palette_generator/CHANGELOG.md index ec6f4e8baf77..7824f212e267 100644 --- a/packages/palette_generator/CHANGELOG.md +++ b/packages/palette_generator/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. * Aligns Dart and Flutter SDK constraints. ## 0.3.3+2 diff --git a/packages/palette_generator/example/.pluginToolsConfig.yaml b/packages/palette_generator/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/palette_generator/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/palette_generator/example/macos/Runner.xcodeproj/project.pbxproj b/packages/palette_generator/example/macos/Runner.xcodeproj/project.pbxproj index 331479d3989a..0007b2234975 100644 --- a/packages/palette_generator/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/palette_generator/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -26,10 +26,6 @@ 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 */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -49,8 +45,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -69,13 +63,11 @@ 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 = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; 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 = ""; }; 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 = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -83,8 +75,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -138,8 +128,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; @@ -194,7 +182,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -247,6 +235,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -260,7 +249,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + 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; @@ -356,7 +345,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -439,7 +428,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -486,7 +475,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/palette_generator/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/palette_generator/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index df12c333e68c..ad089fa5dfb1 100644 --- a/packages/palette_generator/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/palette_generator/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/palette_generator/pubspec.yaml b/packages/palette_generator/pubspec.yaml index a44c21402d28..ad4ed9534a2c 100644 --- a/packages/palette_generator/pubspec.yaml +++ b/packages/palette_generator/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 0.3.3+2 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: collection: ^1.15.0 diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index 5c52ff5d2efd..9f76a52afdd0 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Updates minimum supported macOS version to 10.14. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 2.0.15 + +* Updates iOS minimum version in README. + ## 2.0.14 * Updates README to use code excerpts. diff --git a/packages/path_provider/path_provider/README.md b/packages/path_provider/path_provider/README.md index 648f722caed0..7bee646658d2 100644 --- a/packages/path_provider/path_provider/README.md +++ b/packages/path_provider/path_provider/README.md @@ -7,9 +7,9 @@ A Flutter plugin for finding commonly used locations on the filesystem. Supports Android, iOS, Linux, macOS and Windows. Not all methods are supported on all platforms. -| | Android | iOS | Linux | macOS | Windows | -|-------------|---------|------|-------|--------|-------------| -| **Support** | SDK 16+ | 9.0+ | Any | 10.11+ | Windows 10+ | +| | Android | iOS | Linux | macOS | Windows | +|-------------|---------|-------|-------|--------|-------------| +| **Support** | SDK 16+ | 11.0+ | Any | 10.14+ | Windows 10+ | ## Usage diff --git a/packages/path_provider/path_provider/example/.pluginToolsConfig.yaml b/packages/path_provider/path_provider/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/path_provider/path_provider/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/path_provider/path_provider/example/ios/Flutter/AppFrameworkInfo.plist b/packages/path_provider/path_provider/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f96d4..9b41e7d87980 100644 --- a/packages/path_provider/path_provider/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/path_provider/path_provider/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/path_provider/path_provider/example/ios/Podfile b/packages/path_provider/path_provider/example/ios/Podfile index 3924e59aa0f9..ec43b513b0d1 100644 --- a/packages/path_provider/path_provider/example/ios/Podfile +++ b/packages/path_provider/path_provider/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/project.pbxproj index 86528407809b..f8622bafafdf 100644 --- a/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -222,7 +222,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -300,6 +300,7 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -314,6 +315,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -443,7 +445,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -493,7 +495,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8501fd2bb642..ec3713b95db5 100644 --- a/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/path_provider/path_provider/example/linux/flutter/CMakeLists.txt b/packages/path_provider/path_provider/example/linux/flutter/CMakeLists.txt index 4f48a7ced5f4..94f43ff7fa6a 100644 --- a/packages/path_provider/path_provider/example/linux/flutter/CMakeLists.txt +++ b/packages/path_provider/path_provider/example/linux/flutter/CMakeLists.txt @@ -24,7 +24,6 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) -pkg_check_modules(BLKID REQUIRED IMPORTED_TARGET blkid) set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") @@ -66,7 +65,6 @@ target_link_libraries(flutter INTERFACE PkgConfig::GTK PkgConfig::GLIB PkgConfig::GIO - PkgConfig::BLKID ) add_dependencies(flutter flutter_assemble) diff --git a/packages/path_provider/path_provider/example/macos/Podfile b/packages/path_provider/path_provider/example/macos/Podfile index dade8dfad0dc..049abe295427 100644 --- a/packages/path_provider/path_provider/example/macos/Podfile +++ b/packages/path_provider/path_provider/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/path_provider/path_provider/example/macos/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider/example/macos/Runner.xcodeproj/project.pbxproj index 1e39683e1446..6f03fab76ce9 100644 --- a/packages/path_provider/path_provider/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/path_provider/path_provider/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -27,10 +27,6 @@ 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 */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -50,8 +46,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -72,14 +66,12 @@ 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 = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; 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 = ""; }; 46139048DB9F59D473B61B5E /* 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 = ""; }; 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 = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; F4586DA69948E3A954A2FC9C /* 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 */ @@ -88,8 +80,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, 23F6FAA3AF82DFCF2B7DD79A /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -156,8 +146,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; @@ -215,7 +203,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -268,6 +256,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -281,7 +270,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + 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; @@ -308,10 +297,13 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework", ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -414,7 +406,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -497,7 +489,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -544,7 +536,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/path_provider/path_provider/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/path_provider/path_provider/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1552901c04e0..6ed688ef37b1 100644 --- a/packages/path_provider/path_provider/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/path_provider/path_provider/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index 51d143cb788e..46507ce90a79 100644 --- a/packages/path_provider/path_provider/pubspec.yaml +++ b/packages/path_provider/path_provider/pubspec.yaml @@ -2,11 +2,11 @@ name: path_provider description: Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. repository: https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.14 +version: 2.0.15 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/path_provider/path_provider_android/CHANGELOG.md b/packages/path_provider/path_provider_android/CHANGELOG.md index 5f5514198164..816457564de8 100644 --- a/packages/path_provider/path_provider_android/CHANGELOG.md +++ b/packages/path_provider/path_provider_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.0.27 * Fixes compatibility with AGP versions older than 4.2. diff --git a/packages/path_provider/path_provider_android/example/.pluginToolsConfig.yaml b/packages/path_provider/path_provider_android/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/path_provider/path_provider_android/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/path_provider/path_provider_android/example/pubspec.yaml b/packages/path_provider/path_provider_android/example/pubspec.yaml index d5e08db83fd1..ea9f0877aa26 100644 --- a/packages/path_provider/path_provider_android/example/pubspec.yaml +++ b/packages/path_provider/path_provider_android/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the path_provider plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_android/pubspec.yaml b/packages/path_provider/path_provider_android/pubspec.yaml index f22706b08a7d..2bd685bb6406 100644 --- a/packages/path_provider/path_provider_android/pubspec.yaml +++ b/packages/path_provider/path_provider_android/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.0.27 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/path_provider/path_provider_foundation/CHANGELOG.md b/packages/path_provider/path_provider_foundation/CHANGELOG.md index d25e953a33d0..51dbbfdabb50 100644 --- a/packages/path_provider/path_provider_foundation/CHANGELOG.md +++ b/packages/path_provider/path_provider_foundation/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum supported macOS version to 10.14. + +## 2.2.3 + +* Updates minimum iOS version to 11. + ## 2.2.2 * Updates pigeon for null value handling fixes. diff --git a/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation.podspec b/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation.podspec index fbf9d8e839b9..da786f81bda7 100644 --- a/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation.podspec +++ b/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation.podspec @@ -15,8 +15,8 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' - s.ios.deployment_target = '9.0' - s.osx.deployment_target = '10.11' + s.ios.deployment_target = '11.0' + s.osx.deployment_target = '10.14' s.ios.xcconfig = { 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', diff --git a/packages/path_provider/path_provider_foundation/example/macos/Podfile b/packages/path_provider/path_provider_foundation/example/macos/Podfile index e8da8332969a..47c1b18fedae 100644 --- a/packages/path_provider/path_provider_foundation/example/macos/Podfile +++ b/packages/path_provider/path_provider_foundation/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/project.pbxproj index 5abc18a86297..e6803a68b24b 100644 --- a/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -260,7 +260,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1250; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -325,6 +325,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -509,7 +510,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -592,7 +593,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -639,7 +640,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a0f91afed8ea..c35107a8e2d8 100644 --- a/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =2.18.0 <4.0.0" diff --git a/packages/path_provider/path_provider_linux/CHANGELOG.md b/packages/path_provider/path_provider_linux/CHANGELOG.md index 0604570ee18f..7b4f83da3fd9 100644 --- a/packages/path_provider/path_provider_linux/CHANGELOG.md +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.1.11 + +* Removes obsolete null checks on non-nullable values. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.1.10 * Clarifies explanation of endorsement in README. diff --git a/packages/path_provider/path_provider_linux/example/linux/flutter/CMakeLists.txt b/packages/path_provider/path_provider_linux/example/linux/flutter/CMakeLists.txt index 4f48a7ced5f4..94f43ff7fa6a 100644 --- a/packages/path_provider/path_provider_linux/example/linux/flutter/CMakeLists.txt +++ b/packages/path_provider/path_provider_linux/example/linux/flutter/CMakeLists.txt @@ -24,7 +24,6 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) -pkg_check_modules(BLKID REQUIRED IMPORTED_TARGET blkid) set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") @@ -66,7 +65,6 @@ target_link_libraries(flutter INTERFACE PkgConfig::GTK PkgConfig::GLIB PkgConfig::GIO - PkgConfig::BLKID ) add_dependencies(flutter flutter_assemble) diff --git a/packages/path_provider/path_provider_linux/example/pubspec.yaml b/packages/path_provider/path_provider_linux/example/pubspec.yaml index 088420d57bf3..47ceb172bc0d 100644 --- a/packages/path_provider/path_provider_linux/example/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the path_provider_linux plugin. publish_to: "none" environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_linux/lib/src/get_application_id_real.dart b/packages/path_provider/path_provider_linux/lib/src/get_application_id_real.dart index f01c3e4ee15e..5e16df06c410 100644 --- a/packages/path_provider/path_provider_linux/lib/src/get_application_id_real.dart +++ b/packages/path_provider/path_provider_linux/lib/src/get_application_id_real.dart @@ -71,7 +71,7 @@ String? getApplicationId() { return null; } final Pointer appId = gio.gApplicationGetApplicationId(app); - if (appId == null || appId == nullptr) { + if (appId == nullptr) { return null; } return appId.toDartString(); diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml index 89123c090e00..a72228fd93d4 100644 --- a/packages/path_provider/path_provider_linux/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/pubspec.yaml @@ -2,11 +2,11 @@ name: path_provider_linux description: Linux implementation of the path_provider plugin repository: https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.1.10 +version: 2.1.11 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md index 7dbca936bc3f..f96711be4fdd 100644 --- a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md +++ b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. * Aligns Dart and Flutter SDK constraints. ## 2.0.6 diff --git a/packages/path_provider/path_provider_platform_interface/pubspec.yaml b/packages/path_provider/path_provider_platform_interface/pubspec.yaml index c4c5ca41f8c9..193bb85b94a4 100644 --- a/packages/path_provider/path_provider_platform_interface/pubspec.yaml +++ b/packages/path_provider/path_provider_platform_interface/pubspec.yaml @@ -7,8 +7,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.0.6 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_windows/CHANGELOG.md b/packages/path_provider/path_provider_windows/CHANGELOG.md index a9218dea01a6..beb6e41d32e0 100644 --- a/packages/path_provider/path_provider_windows/CHANGELOG.md +++ b/packages/path_provider/path_provider_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.1.6 * Adds compatibility with `win32` 4.x. diff --git a/packages/path_provider/path_provider_windows/example/pubspec.yaml b/packages/path_provider/path_provider_windows/example/pubspec.yaml index c774bbb20195..5639d5c6fe44 100644 --- a/packages/path_provider/path_provider_windows/example/pubspec.yaml +++ b/packages/path_provider/path_provider_windows/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the path_provider plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_windows/pubspec.yaml b/packages/path_provider/path_provider_windows/pubspec.yaml index 132612c161ef..45f178846ee0 100644 --- a/packages/path_provider/path_provider_windows/pubspec.yaml +++ b/packages/path_provider/path_provider_windows/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.1.6 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index 3a66533e6ef3..fe7793e8b31f 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,5 +1,10 @@ -## NEXT +## 10.0.0 +* [swift] Avoids using `Any` to represent `Optional` in Swift. +* [swift] **Breaking Change** A raw `List` (without generic type argument) in Dart will be + translated into `[Any?]` (rather than `[Any]`) in Swift. +* [swift] **Breaking Change** A raw `Map` (without generic type argument) in Dart will be + translated into `[AnyHashable:Any?]` (rather than `[AnyHashable:Any]`) in Swift. * Adds an example application that uses Pigeon directly, rather than in a plugin. ## 9.2.5 diff --git a/packages/pigeon/example/app/.pluginToolsConfig.yaml b/packages/pigeon/example/app/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/pigeon/example/app/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/pigeon/example/app/android/gradle/wrapper/gradle-wrapper.properties b/packages/pigeon/example/app/android/gradle/wrapper/gradle-wrapper.properties index 3c472b99c6f3..ceccc3a85403 100644 --- a/packages/pigeon/example/app/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/pigeon/example/app/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/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart index 0402185f5563..7508c566d771 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.5'; +const String pigeonVersion = '10.0.0'; /// 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 265052ebb508..1ef6e8a900b1 100644 --- a/packages/pigeon/lib/pigeon_lib.dart +++ b/packages/pigeon/lib/pigeon_lib.dart @@ -1004,6 +1004,9 @@ class _RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { final dart_ast.NamedType? namedType = getFirstChildOfType(parameter); if (namedType != null) { + // TODO(stuartmorgan): Replace `name` when adopting the next version of + // analyzer. + // ignore: deprecated_member_use final String argTypeBaseName = namedType.name.name; final bool isNullable = namedType.question != null; final List argTypeArguments = @@ -1086,6 +1089,9 @@ class _RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { Method( name: node.name.lexeme, returnType: TypeDeclaration( + // TODO(stuartmorgan): Replace `name` when adopting the next + // version of analyzer. + // ignore: deprecated_member_use baseName: returnType.name.name, typeArguments: typeAnnotationsToTypeArguments(returnType.typeArguments), @@ -1135,6 +1141,9 @@ class _RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { for (final Object x in typeArguments.childEntities) { if (x is dart_ast.NamedType) { result.add(TypeDeclaration( + // TODO(stuartmorgan): Replace `name` when adopting the next + // version of analyzer. + // ignore: deprecated_member_use baseName: x.name.name, isNullable: x.question != null, typeArguments: typeAnnotationsToTypeArguments(x.typeArguments))); @@ -1165,6 +1174,9 @@ class _RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { final dart_ast.TypeArgumentList? typeArguments = type.typeArguments; _currentClass!.fields.add(NamedType( type: TypeDeclaration( + // TODO(stuartmorgan): Replace `name` when adopting the next + // version of analyzer. + // ignore: deprecated_member_use baseName: type.name.name, isNullable: type.question != null, typeArguments: typeAnnotationsToTypeArguments(typeArguments), diff --git a/packages/pigeon/lib/swift_generator.dart b/packages/pigeon/lib/swift_generator.dart index 521e2dfb9e1b..a133a24ed4e1 100644 --- a/packages/pigeon/lib/swift_generator.dart +++ b/packages/pigeon/lib/swift_generator.dart @@ -169,7 +169,7 @@ import FlutterMacOS Set customEnumNames, ) { final String className = klass.name; - indent.write('static func fromList(_ list: [Any]) -> $className? '); + indent.write('static func fromList(_ list: [Any?]) -> $className? '); indent.addScoped('{', '}', () { enumerate(getFieldsInSerializationOrder(klass), @@ -431,7 +431,7 @@ import FlutterMacOS indent.addScoped('{ $messageVarName, reply in', '}', () { final List methodArgument = []; if (components.arguments.isNotEmpty) { - indent.writeln('let args = message as! [Any]'); + indent.writeln('let args = message as! [Any?]'); enumerate(components.arguments, (int index, _SwiftFunctionArgument arg) { final String argName = @@ -524,7 +524,7 @@ import FlutterMacOS indent.writeln('case ${customClass.enumeration}:'); indent.nest(1, () { indent.writeln( - 'return ${customClass.name}.fromList(self.readValue() as! [Any])'); + 'return ${customClass.name}.fromList(self.readValue() as! [Any?])'); }); } indent.writeln('default:'); @@ -605,8 +605,7 @@ import FlutterMacOS 'nullable enums require special code that this helper does not supply'); return '${_swiftTypeForDartType(type)}(rawValue: $value as! Int)!'; } else if (type.baseName == 'Object') { - // Special-cased to avoid warnings about using 'as' with Any. - return value; + return value + (type.isNullable ? '' : '!'); } else if (type.baseName == 'int') { if (type.isNullable) { // Nullable ints need to check for NSNull, and Int32 before casting can be done safely. @@ -628,7 +627,7 @@ import FlutterMacOS if (listEncodedClassNames != null && listEncodedClassNames.contains(type.baseName)) { indent.writeln('var $variableName: $fieldType? = nil'); - indent.write('if let ${variableName}List = $value as! [Any]? '); + indent.write('if let ${variableName}List = $value as! [Any?]? '); indent.addScoped('{', '}', () { indent.writeln( '$variableName = $fieldType.fromList(${variableName}List)'); @@ -652,7 +651,7 @@ import FlutterMacOS if (listEncodedClassNames != null && listEncodedClassNames.contains(type.baseName)) { indent.writeln( - 'let $variableName = $fieldType.fromList($value as! [Any])!'); + 'let $variableName = $fieldType.fromList($value as! [Any?])!'); } else { indent.writeln( 'let $variableName = ${castForceUnwrap(value, type, root)}'); @@ -695,7 +694,7 @@ import FlutterMacOS private func nilOrValue(_ value: Any?) -> T? { if value is NSNull { return nil } - return (value as Any) as! T? + return value as! T? }'''); } @@ -739,9 +738,9 @@ String _flattenTypeArguments(List args) { String _swiftTypeForBuiltinGenericDartType(TypeDeclaration type) { if (type.typeArguments.isEmpty) { if (type.baseName == 'List') { - return '[Any]'; + return '[Any?]'; } else if (type.baseName == 'Map') { - return '[AnyHashable: Any]'; + return '[AnyHashable: Any?]'; } else { return 'Any'; } diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/alternate_language_test_plugin.podspec b/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/alternate_language_test_plugin.podspec index d557b580b069..9690715430d7 100644 --- a/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/alternate_language_test_plugin.podspec +++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/ios/alternate_language_test_plugin.podspec @@ -16,7 +16,7 @@ A plugin to test Pigeon generation for secondary languages (e.g., Java, Objectiv s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.platform = :ios, '9.0' + s.platform = :ios, '11.0' s.xcconfig = { 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', diff --git a/packages/pigeon/platform_tests/test_plugin/example/ios/Runner.xcodeproj/project.pbxproj b/packages/pigeon/platform_tests/test_plugin/example/ios/Runner.xcodeproj/project.pbxproj index e7d1bfc2f5ab..eb200b273bb5 100644 --- a/packages/pigeon/platform_tests/test_plugin/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/pigeon/platform_tests/test_plugin/example/ios/Runner.xcodeproj/project.pbxproj @@ -542,6 +542,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; @@ -557,6 +558,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; @@ -572,6 +574,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; diff --git a/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/EchoBinaryMessenger.swift b/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/EchoBinaryMessenger.swift index fe2886ff25c7..fef7e2d1b4c5 100644 --- a/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/EchoBinaryMessenger.swift +++ b/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/EchoBinaryMessenger.swift @@ -28,13 +28,14 @@ class EchoBinaryMessenger: NSObject, FlutterBinaryMessenger { guard let args = self.codec.decode(message) as? [Any?], - let firstArg: Any? = nilOrValue(args.first) + let firstArg = args.first, + let castedFirstArg: Any? = nilOrValue(firstArg) else { callback(self.defaultReturn.flatMap { self.codec.encode($0) }) return } - callback(self.codec.encode(firstArg)) + callback(self.codec.encode(castedFirstArg)) } func setMessageHandlerOnChannel( diff --git a/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/NullableReturnsTests.swift b/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/NullableReturnsTests.swift index a12eb07b414b..384c9a27ba1b 100644 --- a/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/NullableReturnsTests.swift +++ b/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/NullableReturnsTests.swift @@ -40,7 +40,7 @@ class NullableReturnsTests: XCTestCase { NullableArgHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: api) XCTAssertNotNil(binaryMessenger.handlers[channel]) - let inputEncoded = binaryMessenger.codec.encode([nil]) + let inputEncoded = binaryMessenger.codec.encode([nil] as [Any?]) let expectation = XCTestExpectation(description: "callback") binaryMessenger.handlers[channel]?(inputEncoded) { _ in diff --git a/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/PrimitiveTests.swift b/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/PrimitiveTests.swift index c0a55ff506db..451b8d7481ac 100644 --- a/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/PrimitiveTests.swift +++ b/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/PrimitiveTests.swift @@ -11,8 +11,8 @@ class MockPrimitiveHostApi: PrimitiveHostApi { func aBool(value: Bool) -> Bool { value } func aString(value: String) -> String { value } func aDouble(value: Double) -> Double { value } - func aMap(value: [AnyHashable: Any]) -> [AnyHashable: Any] { value } - func aList(value: [Any]) -> [Any] { value } + func aMap(value: [AnyHashable: Any?]) -> [AnyHashable: Any?] { value } + func aList(value: [Any?]) -> [Any?] { value } func anInt32List(value: FlutterStandardTypedData) -> FlutterStandardTypedData { value } func aBoolList(value: [Bool?]) -> [Bool?] { value } func aStringIntMap(value: [String?: Int64?]) -> [String?: Int64?] { value } diff --git a/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift b/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift index f6e7d80b616e..2590ee6aa87c 100644 --- a/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift +++ b/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift @@ -35,7 +35,7 @@ private func wrapError(_ error: Any) -> [Any?] { private func nilOrValue(_ value: Any?) -> T? { if value is NSNull { return nil } - return (value as Any) as! T? + return value as! T? } enum AnEnum: Int { @@ -54,12 +54,12 @@ struct AllTypes { var a4ByteArray: FlutterStandardTypedData var a8ByteArray: FlutterStandardTypedData var aFloatArray: FlutterStandardTypedData - var aList: [Any] - var aMap: [AnyHashable: Any] + var aList: [Any?] + var aMap: [AnyHashable: Any?] var anEnum: AnEnum var aString: String - static func fromList(_ list: [Any]) -> AllTypes? { + static func fromList(_ list: [Any?]) -> AllTypes? { let aBool = list[0] as! Bool let anInt = list[1] is Int64 ? list[1] as! Int64 : Int64(list[1] as! Int32) let anInt64 = list[2] is Int64 ? list[2] as! Int64 : Int64(list[2] as! Int32) @@ -68,8 +68,8 @@ struct AllTypes { let a4ByteArray = list[5] as! FlutterStandardTypedData let a8ByteArray = list[6] as! FlutterStandardTypedData let aFloatArray = list[7] as! FlutterStandardTypedData - let aList = list[8] as! [Any] - let aMap = list[9] as! [AnyHashable: Any] + let aList = list[8] as! [Any?] + let aMap = list[9] as! [AnyHashable: Any?] let anEnum = AnEnum(rawValue: list[10] as! Int)! let aString = list[11] as! String @@ -116,15 +116,15 @@ struct AllNullableTypes { var aNullable4ByteArray: FlutterStandardTypedData? = nil var aNullable8ByteArray: FlutterStandardTypedData? = nil var aNullableFloatArray: FlutterStandardTypedData? = nil - var aNullableList: [Any]? = nil - var aNullableMap: [AnyHashable: Any]? = nil + var aNullableList: [Any?]? = nil + var aNullableMap: [AnyHashable: Any?]? = nil var nullableNestedList: [[Bool?]?]? = nil var nullableMapWithAnnotations: [String?: String?]? = nil var nullableMapWithObject: [String?: Any?]? = nil var aNullableEnum: AnEnum? = nil var aNullableString: String? = nil - static func fromList(_ list: [Any]) -> AllNullableTypes? { + static func fromList(_ list: [Any?]) -> AllNullableTypes? { let aNullableBool: Bool? = nilOrValue(list[0]) let aNullableInt: Int64? = list[1] is NSNull ? nil : (list[1] is Int64? ? list[1] as! Int64? : Int64(list[1] as! Int32)) let aNullableInt64: Int64? = list[2] is NSNull ? nil : (list[2] is Int64? ? list[2] as! Int64? : Int64(list[2] as! Int32)) @@ -133,8 +133,8 @@ struct AllNullableTypes { let aNullable4ByteArray: FlutterStandardTypedData? = nilOrValue(list[5]) let aNullable8ByteArray: FlutterStandardTypedData? = nilOrValue(list[6]) let aNullableFloatArray: FlutterStandardTypedData? = nilOrValue(list[7]) - let aNullableList: [Any]? = nilOrValue(list[8]) - let aNullableMap: [AnyHashable: Any]? = nilOrValue(list[9]) + let aNullableList: [Any?]? = nilOrValue(list[8]) + let aNullableMap: [AnyHashable: Any?]? = nilOrValue(list[9]) let nullableNestedList: [[Bool?]?]? = nilOrValue(list[10]) let nullableMapWithAnnotations: [String?: String?]? = nilOrValue(list[11]) let nullableMapWithObject: [String?: Any?]? = nilOrValue(list[12]) @@ -188,8 +188,8 @@ struct AllNullableTypes { struct AllNullableTypesWrapper { var values: AllNullableTypes - static func fromList(_ list: [Any]) -> AllNullableTypesWrapper? { - let values = AllNullableTypes.fromList(list[0] as! [Any])! + static func fromList(_ list: [Any?]) -> AllNullableTypesWrapper? { + let values = AllNullableTypes.fromList(list[0] as! [Any?])! return AllNullableTypesWrapper( values: values @@ -206,10 +206,10 @@ struct AllNullableTypesWrapper { /// /// Generated class from Pigeon that represents data sent in messages. struct TestMessage { - var testList: [Any]? = nil + var testList: [Any?]? = nil - static func fromList(_ list: [Any]) -> TestMessage? { - let testList: [Any]? = nilOrValue(list[0]) + static func fromList(_ list: [Any?]) -> TestMessage? { + let testList: [Any?]? = nilOrValue(list[0]) return TestMessage( testList: testList @@ -226,13 +226,13 @@ private class HostIntegrationCoreApiCodecReader: FlutterStandardReader { override func readValue(ofType type: UInt8) -> Any? { switch type { case 128: - return AllNullableTypes.fromList(self.readValue() as! [Any]) + return AllNullableTypes.fromList(self.readValue() as! [Any?]) case 129: - return AllNullableTypesWrapper.fromList(self.readValue() as! [Any]) + return AllNullableTypesWrapper.fromList(self.readValue() as! [Any?]) case 130: - return AllTypes.fromList(self.readValue() as! [Any]) + return AllTypes.fromList(self.readValue() as! [Any?]) case 131: - return TestMessage.fromList(self.readValue() as! [Any]) + return TestMessage.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) } @@ -422,7 +422,7 @@ class HostIntegrationCoreApiSetup { let echoAllTypesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAllTypes", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAllTypesChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let everythingArg = args[0] as! AllTypes do { let result = try api.echo(everythingArg) @@ -480,7 +480,7 @@ class HostIntegrationCoreApiSetup { let echoIntChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoInt", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoIntChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let anIntArg = args[0] is Int64 ? args[0] as! Int64 : Int64(args[0] as! Int32) do { let result = try api.echo(anIntArg) @@ -496,7 +496,7 @@ class HostIntegrationCoreApiSetup { let echoDoubleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoDouble", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoDoubleChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aDoubleArg = args[0] as! Double do { let result = try api.echo(aDoubleArg) @@ -512,7 +512,7 @@ class HostIntegrationCoreApiSetup { let echoBoolChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoBool", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoBoolChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aBoolArg = args[0] as! Bool do { let result = try api.echo(aBoolArg) @@ -528,7 +528,7 @@ class HostIntegrationCoreApiSetup { let echoStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aStringArg = args[0] as! String do { let result = try api.echo(aStringArg) @@ -544,7 +544,7 @@ class HostIntegrationCoreApiSetup { let echoUint8ListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoUint8List", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoUint8ListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aUint8ListArg = args[0] as! FlutterStandardTypedData do { let result = try api.echo(aUint8ListArg) @@ -560,8 +560,8 @@ class HostIntegrationCoreApiSetup { let echoObjectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoObject", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoObjectChannel.setMessageHandler { message, reply in - let args = message as! [Any] - let anObjectArg = args[0] + let args = message as! [Any?] + let anObjectArg = args[0]! do { let result = try api.echo(anObjectArg) reply(wrapResult(result)) @@ -576,7 +576,7 @@ class HostIntegrationCoreApiSetup { let echoListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoList", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aListArg = args[0] as! [Any?] do { let result = try api.echo(aListArg) @@ -592,7 +592,7 @@ class HostIntegrationCoreApiSetup { let echoMapChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoMap", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoMapChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aMapArg = args[0] as! [String?: Any?] do { let result = try api.echo(aMapArg) @@ -608,7 +608,7 @@ class HostIntegrationCoreApiSetup { let echoAllNullableTypesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAllNullableTypes", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAllNullableTypesChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let everythingArg: AllNullableTypes? = nilOrValue(args[0]) do { let result = try api.echo(everythingArg) @@ -625,7 +625,7 @@ class HostIntegrationCoreApiSetup { let extractNestedNullableStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.extractNestedNullableString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { extractNestedNullableStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let wrapperArg = args[0] as! AllNullableTypesWrapper do { let result = try api.extractNestedNullableString(from: wrapperArg) @@ -642,7 +642,7 @@ class HostIntegrationCoreApiSetup { let createNestedNullableStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.createNestedNullableString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { createNestedNullableStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let nullableStringArg: String? = nilOrValue(args[0]) do { let result = try api.createNestedObject(with: nullableStringArg) @@ -658,7 +658,7 @@ class HostIntegrationCoreApiSetup { let sendMultipleNullableTypesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.sendMultipleNullableTypes", binaryMessenger: binaryMessenger, codec: codec) if let api = api { sendMultipleNullableTypesChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableBoolArg: Bool? = nilOrValue(args[0]) let aNullableIntArg: Int64? = args[1] is NSNull ? nil : (args[1] is Int64? ? args[1] as! Int64? : Int64(args[1] as! Int32)) let aNullableStringArg: String? = nilOrValue(args[2]) @@ -676,7 +676,7 @@ class HostIntegrationCoreApiSetup { let echoNullableIntChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableInt", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableIntChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableIntArg: Int64? = args[0] is NSNull ? nil : (args[0] is Int64? ? args[0] as! Int64? : Int64(args[0] as! Int32)) do { let result = try api.echo(aNullableIntArg) @@ -692,7 +692,7 @@ class HostIntegrationCoreApiSetup { let echoNullableDoubleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableDouble", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableDoubleChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableDoubleArg: Double? = nilOrValue(args[0]) do { let result = try api.echo(aNullableDoubleArg) @@ -708,7 +708,7 @@ class HostIntegrationCoreApiSetup { let echoNullableBoolChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableBool", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableBoolChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableBoolArg: Bool? = nilOrValue(args[0]) do { let result = try api.echo(aNullableBoolArg) @@ -724,7 +724,7 @@ class HostIntegrationCoreApiSetup { let echoNullableStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableStringArg: String? = nilOrValue(args[0]) do { let result = try api.echo(aNullableStringArg) @@ -740,7 +740,7 @@ class HostIntegrationCoreApiSetup { let echoNullableUint8ListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableUint8List", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableUint8ListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableUint8ListArg: FlutterStandardTypedData? = nilOrValue(args[0]) do { let result = try api.echo(aNullableUint8ListArg) @@ -756,7 +756,7 @@ class HostIntegrationCoreApiSetup { let echoNullableObjectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableObject", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableObjectChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableObjectArg: Any? = args[0] do { let result = try api.echo(aNullableObjectArg) @@ -772,7 +772,7 @@ class HostIntegrationCoreApiSetup { let echoNullableListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableList", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableListArg: [Any?]? = nilOrValue(args[0]) do { let result = try api.echoNullable(aNullableListArg) @@ -788,7 +788,7 @@ class HostIntegrationCoreApiSetup { let echoNullableMapChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableMap", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableMapChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableMapArg: [String?: Any?]? = nilOrValue(args[0]) do { let result = try api.echoNullable(aNullableMapArg) @@ -821,7 +821,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncIntChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncInt", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncIntChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let anIntArg = args[0] is Int64 ? args[0] as! Int64 : Int64(args[0] as! Int32) api.echoAsync(anIntArg) { result in switch result { @@ -839,7 +839,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncDoubleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncDouble", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncDoubleChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aDoubleArg = args[0] as! Double api.echoAsync(aDoubleArg) { result in switch result { @@ -857,7 +857,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncBoolChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncBool", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncBoolChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aBoolArg = args[0] as! Bool api.echoAsync(aBoolArg) { result in switch result { @@ -875,7 +875,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aStringArg = args[0] as! String api.echoAsync(aStringArg) { result in switch result { @@ -893,7 +893,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncUint8ListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncUint8List", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncUint8ListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aUint8ListArg = args[0] as! FlutterStandardTypedData api.echoAsync(aUint8ListArg) { result in switch result { @@ -911,8 +911,8 @@ class HostIntegrationCoreApiSetup { let echoAsyncObjectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncObject", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncObjectChannel.setMessageHandler { message, reply in - let args = message as! [Any] - let anObjectArg = args[0] + let args = message as! [Any?] + let anObjectArg = args[0]! api.echoAsync(anObjectArg) { result in switch result { case .success(let res): @@ -929,7 +929,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncList", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aListArg = args[0] as! [Any?] api.echoAsync(aListArg) { result in switch result { @@ -947,7 +947,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncMapChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncMap", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncMapChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aMapArg = args[0] as! [String?: Any?] api.echoAsync(aMapArg) { result in switch result { @@ -1013,7 +1013,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncAllTypesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncAllTypes", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncAllTypesChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let everythingArg = args[0] as! AllTypes api.echoAsync(everythingArg) { result in switch result { @@ -1031,7 +1031,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableAllNullableTypesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableAllNullableTypes", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableAllNullableTypesChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let everythingArg: AllNullableTypes? = nilOrValue(args[0]) api.echoAsync(everythingArg) { result in switch result { @@ -1049,7 +1049,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableIntChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableInt", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableIntChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let anIntArg: Int64? = args[0] is NSNull ? nil : (args[0] is Int64? ? args[0] as! Int64? : Int64(args[0] as! Int32)) api.echoAsyncNullable(anIntArg) { result in switch result { @@ -1067,7 +1067,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableDoubleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableDouble", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableDoubleChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aDoubleArg: Double? = nilOrValue(args[0]) api.echoAsyncNullable(aDoubleArg) { result in switch result { @@ -1085,7 +1085,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableBoolChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableBool", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableBoolChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aBoolArg: Bool? = nilOrValue(args[0]) api.echoAsyncNullable(aBoolArg) { result in switch result { @@ -1103,7 +1103,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aStringArg: String? = nilOrValue(args[0]) api.echoAsyncNullable(aStringArg) { result in switch result { @@ -1121,7 +1121,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableUint8ListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableUint8List", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableUint8ListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aUint8ListArg: FlutterStandardTypedData? = nilOrValue(args[0]) api.echoAsyncNullable(aUint8ListArg) { result in switch result { @@ -1139,7 +1139,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableObjectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableObject", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableObjectChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let anObjectArg: Any? = args[0] api.echoAsyncNullable(anObjectArg) { result in switch result { @@ -1157,7 +1157,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableList", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aListArg: [Any?]? = nilOrValue(args[0]) api.echoAsyncNullable(aListArg) { result in switch result { @@ -1175,7 +1175,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableMapChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableMap", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableMapChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aMapArg: [String?: Any?]? = nilOrValue(args[0]) api.echAsyncoNullable(aMapArg) { result in switch result { @@ -1237,7 +1237,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoAllTypesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoAllTypes", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoAllTypesChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let everythingArg = args[0] as! AllTypes api.callFlutterEcho(everythingArg) { result in switch result { @@ -1254,7 +1254,7 @@ class HostIntegrationCoreApiSetup { let callFlutterSendMultipleNullableTypesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterSendMultipleNullableTypes", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterSendMultipleNullableTypesChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableBoolArg: Bool? = nilOrValue(args[0]) let aNullableIntArg: Int64? = args[1] is NSNull ? nil : (args[1] is Int64? ? args[1] as! Int64? : Int64(args[1] as! Int32)) let aNullableStringArg: String? = nilOrValue(args[2]) @@ -1273,7 +1273,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoBoolChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoBool", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoBoolChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aBoolArg = args[0] as! Bool api.callFlutterEcho(aBoolArg) { result in switch result { @@ -1290,7 +1290,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoIntChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoInt", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoIntChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let anIntArg = args[0] is Int64 ? args[0] as! Int64 : Int64(args[0] as! Int32) api.callFlutterEcho(anIntArg) { result in switch result { @@ -1307,7 +1307,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoDoubleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoDouble", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoDoubleChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aDoubleArg = args[0] as! Double api.callFlutterEcho(aDoubleArg) { result in switch result { @@ -1324,7 +1324,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aStringArg = args[0] as! String api.callFlutterEcho(aStringArg) { result in switch result { @@ -1341,7 +1341,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoUint8ListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoUint8List", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoUint8ListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aListArg = args[0] as! FlutterStandardTypedData api.callFlutterEcho(aListArg) { result in switch result { @@ -1358,7 +1358,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoList", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aListArg = args[0] as! [Any?] api.callFlutterEcho(aListArg) { result in switch result { @@ -1375,7 +1375,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoMapChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoMap", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoMapChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aMapArg = args[0] as! [String?: Any?] api.callFlutterEcho(aMapArg) { result in switch result { @@ -1392,7 +1392,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoNullableBoolChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoNullableBool", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoNullableBoolChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aBoolArg: Bool? = nilOrValue(args[0]) api.callFlutterEchoNullable(aBoolArg) { result in switch result { @@ -1409,7 +1409,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoNullableIntChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoNullableInt", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoNullableIntChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let anIntArg: Int64? = args[0] is NSNull ? nil : (args[0] is Int64? ? args[0] as! Int64? : Int64(args[0] as! Int32)) api.callFlutterEchoNullable(anIntArg) { result in switch result { @@ -1426,7 +1426,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoNullableDoubleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoNullableDouble", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoNullableDoubleChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aDoubleArg: Double? = nilOrValue(args[0]) api.callFlutterEchoNullable(aDoubleArg) { result in switch result { @@ -1443,7 +1443,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoNullableStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoNullableString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoNullableStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aStringArg: String? = nilOrValue(args[0]) api.callFlutterEchoNullable(aStringArg) { result in switch result { @@ -1460,7 +1460,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoNullableUint8ListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoNullableUint8List", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoNullableUint8ListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aListArg: FlutterStandardTypedData? = nilOrValue(args[0]) api.callFlutterEchoNullable(aListArg) { result in switch result { @@ -1477,7 +1477,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoNullableListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoNullableList", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoNullableListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aListArg: [Any?]? = nilOrValue(args[0]) api.callFlutterEchoNullable(aListArg) { result in switch result { @@ -1494,7 +1494,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoNullableMapChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoNullableMap", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoNullableMapChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aMapArg: [String?: Any?]? = nilOrValue(args[0]) api.callFlutterEchoNullable(aMapArg) { result in switch result { @@ -1514,13 +1514,13 @@ private class FlutterIntegrationCoreApiCodecReader: FlutterStandardReader { override func readValue(ofType type: UInt8) -> Any? { switch type { case 128: - return AllNullableTypes.fromList(self.readValue() as! [Any]) + return AllNullableTypes.fromList(self.readValue() as! [Any?]) case 129: - return AllNullableTypesWrapper.fromList(self.readValue() as! [Any]) + return AllNullableTypesWrapper.fromList(self.readValue() as! [Any?]) case 130: - return AllTypes.fromList(self.readValue() as! [Any]) + return AllTypes.fromList(self.readValue() as! [Any?]) case 131: - return TestMessage.fromList(self.readValue() as! [Any]) + return TestMessage.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) } @@ -1794,7 +1794,7 @@ class HostSmallApiSetup { let echoChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostSmallApi.echo", binaryMessenger: binaryMessenger) if let api = api { echoChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aStringArg = args[0] as! String api.echo(aString: aStringArg) { result in switch result { @@ -1829,7 +1829,7 @@ private class FlutterSmallApiCodecReader: FlutterStandardReader { override func readValue(ofType type: UInt8) -> Any? { switch type { case 128: - return TestMessage.fromList(self.readValue() as! [Any]) + return TestMessage.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) } diff --git a/packages/pigeon/platform_tests/test_plugin/ios/test_plugin.podspec b/packages/pigeon/platform_tests/test_plugin/ios/test_plugin.podspec index fc9e058efe57..9a0d842063c9 100644 --- a/packages/pigeon/platform_tests/test_plugin/ios/test_plugin.podspec +++ b/packages/pigeon/platform_tests/test_plugin/ios/test_plugin.podspec @@ -15,7 +15,7 @@ Pod::Spec.new do |s| s.source = { :http => 'https://github.com/flutter/packages/tree/main/packages/pigeon' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '9.0' + s.platform = :ios, '11.0' s.xcconfig = { 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', diff --git a/packages/pigeon/platform_tests/test_plugin/macos/Classes/CoreTests.gen.swift b/packages/pigeon/platform_tests/test_plugin/macos/Classes/CoreTests.gen.swift index f6e7d80b616e..2590ee6aa87c 100644 --- a/packages/pigeon/platform_tests/test_plugin/macos/Classes/CoreTests.gen.swift +++ b/packages/pigeon/platform_tests/test_plugin/macos/Classes/CoreTests.gen.swift @@ -35,7 +35,7 @@ private func wrapError(_ error: Any) -> [Any?] { private func nilOrValue(_ value: Any?) -> T? { if value is NSNull { return nil } - return (value as Any) as! T? + return value as! T? } enum AnEnum: Int { @@ -54,12 +54,12 @@ struct AllTypes { var a4ByteArray: FlutterStandardTypedData var a8ByteArray: FlutterStandardTypedData var aFloatArray: FlutterStandardTypedData - var aList: [Any] - var aMap: [AnyHashable: Any] + var aList: [Any?] + var aMap: [AnyHashable: Any?] var anEnum: AnEnum var aString: String - static func fromList(_ list: [Any]) -> AllTypes? { + static func fromList(_ list: [Any?]) -> AllTypes? { let aBool = list[0] as! Bool let anInt = list[1] is Int64 ? list[1] as! Int64 : Int64(list[1] as! Int32) let anInt64 = list[2] is Int64 ? list[2] as! Int64 : Int64(list[2] as! Int32) @@ -68,8 +68,8 @@ struct AllTypes { let a4ByteArray = list[5] as! FlutterStandardTypedData let a8ByteArray = list[6] as! FlutterStandardTypedData let aFloatArray = list[7] as! FlutterStandardTypedData - let aList = list[8] as! [Any] - let aMap = list[9] as! [AnyHashable: Any] + let aList = list[8] as! [Any?] + let aMap = list[9] as! [AnyHashable: Any?] let anEnum = AnEnum(rawValue: list[10] as! Int)! let aString = list[11] as! String @@ -116,15 +116,15 @@ struct AllNullableTypes { var aNullable4ByteArray: FlutterStandardTypedData? = nil var aNullable8ByteArray: FlutterStandardTypedData? = nil var aNullableFloatArray: FlutterStandardTypedData? = nil - var aNullableList: [Any]? = nil - var aNullableMap: [AnyHashable: Any]? = nil + var aNullableList: [Any?]? = nil + var aNullableMap: [AnyHashable: Any?]? = nil var nullableNestedList: [[Bool?]?]? = nil var nullableMapWithAnnotations: [String?: String?]? = nil var nullableMapWithObject: [String?: Any?]? = nil var aNullableEnum: AnEnum? = nil var aNullableString: String? = nil - static func fromList(_ list: [Any]) -> AllNullableTypes? { + static func fromList(_ list: [Any?]) -> AllNullableTypes? { let aNullableBool: Bool? = nilOrValue(list[0]) let aNullableInt: Int64? = list[1] is NSNull ? nil : (list[1] is Int64? ? list[1] as! Int64? : Int64(list[1] as! Int32)) let aNullableInt64: Int64? = list[2] is NSNull ? nil : (list[2] is Int64? ? list[2] as! Int64? : Int64(list[2] as! Int32)) @@ -133,8 +133,8 @@ struct AllNullableTypes { let aNullable4ByteArray: FlutterStandardTypedData? = nilOrValue(list[5]) let aNullable8ByteArray: FlutterStandardTypedData? = nilOrValue(list[6]) let aNullableFloatArray: FlutterStandardTypedData? = nilOrValue(list[7]) - let aNullableList: [Any]? = nilOrValue(list[8]) - let aNullableMap: [AnyHashable: Any]? = nilOrValue(list[9]) + let aNullableList: [Any?]? = nilOrValue(list[8]) + let aNullableMap: [AnyHashable: Any?]? = nilOrValue(list[9]) let nullableNestedList: [[Bool?]?]? = nilOrValue(list[10]) let nullableMapWithAnnotations: [String?: String?]? = nilOrValue(list[11]) let nullableMapWithObject: [String?: Any?]? = nilOrValue(list[12]) @@ -188,8 +188,8 @@ struct AllNullableTypes { struct AllNullableTypesWrapper { var values: AllNullableTypes - static func fromList(_ list: [Any]) -> AllNullableTypesWrapper? { - let values = AllNullableTypes.fromList(list[0] as! [Any])! + static func fromList(_ list: [Any?]) -> AllNullableTypesWrapper? { + let values = AllNullableTypes.fromList(list[0] as! [Any?])! return AllNullableTypesWrapper( values: values @@ -206,10 +206,10 @@ struct AllNullableTypesWrapper { /// /// Generated class from Pigeon that represents data sent in messages. struct TestMessage { - var testList: [Any]? = nil + var testList: [Any?]? = nil - static func fromList(_ list: [Any]) -> TestMessage? { - let testList: [Any]? = nilOrValue(list[0]) + static func fromList(_ list: [Any?]) -> TestMessage? { + let testList: [Any?]? = nilOrValue(list[0]) return TestMessage( testList: testList @@ -226,13 +226,13 @@ private class HostIntegrationCoreApiCodecReader: FlutterStandardReader { override func readValue(ofType type: UInt8) -> Any? { switch type { case 128: - return AllNullableTypes.fromList(self.readValue() as! [Any]) + return AllNullableTypes.fromList(self.readValue() as! [Any?]) case 129: - return AllNullableTypesWrapper.fromList(self.readValue() as! [Any]) + return AllNullableTypesWrapper.fromList(self.readValue() as! [Any?]) case 130: - return AllTypes.fromList(self.readValue() as! [Any]) + return AllTypes.fromList(self.readValue() as! [Any?]) case 131: - return TestMessage.fromList(self.readValue() as! [Any]) + return TestMessage.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) } @@ -422,7 +422,7 @@ class HostIntegrationCoreApiSetup { let echoAllTypesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAllTypes", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAllTypesChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let everythingArg = args[0] as! AllTypes do { let result = try api.echo(everythingArg) @@ -480,7 +480,7 @@ class HostIntegrationCoreApiSetup { let echoIntChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoInt", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoIntChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let anIntArg = args[0] is Int64 ? args[0] as! Int64 : Int64(args[0] as! Int32) do { let result = try api.echo(anIntArg) @@ -496,7 +496,7 @@ class HostIntegrationCoreApiSetup { let echoDoubleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoDouble", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoDoubleChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aDoubleArg = args[0] as! Double do { let result = try api.echo(aDoubleArg) @@ -512,7 +512,7 @@ class HostIntegrationCoreApiSetup { let echoBoolChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoBool", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoBoolChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aBoolArg = args[0] as! Bool do { let result = try api.echo(aBoolArg) @@ -528,7 +528,7 @@ class HostIntegrationCoreApiSetup { let echoStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aStringArg = args[0] as! String do { let result = try api.echo(aStringArg) @@ -544,7 +544,7 @@ class HostIntegrationCoreApiSetup { let echoUint8ListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoUint8List", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoUint8ListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aUint8ListArg = args[0] as! FlutterStandardTypedData do { let result = try api.echo(aUint8ListArg) @@ -560,8 +560,8 @@ class HostIntegrationCoreApiSetup { let echoObjectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoObject", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoObjectChannel.setMessageHandler { message, reply in - let args = message as! [Any] - let anObjectArg = args[0] + let args = message as! [Any?] + let anObjectArg = args[0]! do { let result = try api.echo(anObjectArg) reply(wrapResult(result)) @@ -576,7 +576,7 @@ class HostIntegrationCoreApiSetup { let echoListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoList", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aListArg = args[0] as! [Any?] do { let result = try api.echo(aListArg) @@ -592,7 +592,7 @@ class HostIntegrationCoreApiSetup { let echoMapChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoMap", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoMapChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aMapArg = args[0] as! [String?: Any?] do { let result = try api.echo(aMapArg) @@ -608,7 +608,7 @@ class HostIntegrationCoreApiSetup { let echoAllNullableTypesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAllNullableTypes", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAllNullableTypesChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let everythingArg: AllNullableTypes? = nilOrValue(args[0]) do { let result = try api.echo(everythingArg) @@ -625,7 +625,7 @@ class HostIntegrationCoreApiSetup { let extractNestedNullableStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.extractNestedNullableString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { extractNestedNullableStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let wrapperArg = args[0] as! AllNullableTypesWrapper do { let result = try api.extractNestedNullableString(from: wrapperArg) @@ -642,7 +642,7 @@ class HostIntegrationCoreApiSetup { let createNestedNullableStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.createNestedNullableString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { createNestedNullableStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let nullableStringArg: String? = nilOrValue(args[0]) do { let result = try api.createNestedObject(with: nullableStringArg) @@ -658,7 +658,7 @@ class HostIntegrationCoreApiSetup { let sendMultipleNullableTypesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.sendMultipleNullableTypes", binaryMessenger: binaryMessenger, codec: codec) if let api = api { sendMultipleNullableTypesChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableBoolArg: Bool? = nilOrValue(args[0]) let aNullableIntArg: Int64? = args[1] is NSNull ? nil : (args[1] is Int64? ? args[1] as! Int64? : Int64(args[1] as! Int32)) let aNullableStringArg: String? = nilOrValue(args[2]) @@ -676,7 +676,7 @@ class HostIntegrationCoreApiSetup { let echoNullableIntChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableInt", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableIntChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableIntArg: Int64? = args[0] is NSNull ? nil : (args[0] is Int64? ? args[0] as! Int64? : Int64(args[0] as! Int32)) do { let result = try api.echo(aNullableIntArg) @@ -692,7 +692,7 @@ class HostIntegrationCoreApiSetup { let echoNullableDoubleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableDouble", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableDoubleChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableDoubleArg: Double? = nilOrValue(args[0]) do { let result = try api.echo(aNullableDoubleArg) @@ -708,7 +708,7 @@ class HostIntegrationCoreApiSetup { let echoNullableBoolChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableBool", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableBoolChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableBoolArg: Bool? = nilOrValue(args[0]) do { let result = try api.echo(aNullableBoolArg) @@ -724,7 +724,7 @@ class HostIntegrationCoreApiSetup { let echoNullableStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableStringArg: String? = nilOrValue(args[0]) do { let result = try api.echo(aNullableStringArg) @@ -740,7 +740,7 @@ class HostIntegrationCoreApiSetup { let echoNullableUint8ListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableUint8List", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableUint8ListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableUint8ListArg: FlutterStandardTypedData? = nilOrValue(args[0]) do { let result = try api.echo(aNullableUint8ListArg) @@ -756,7 +756,7 @@ class HostIntegrationCoreApiSetup { let echoNullableObjectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableObject", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableObjectChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableObjectArg: Any? = args[0] do { let result = try api.echo(aNullableObjectArg) @@ -772,7 +772,7 @@ class HostIntegrationCoreApiSetup { let echoNullableListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableList", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableListArg: [Any?]? = nilOrValue(args[0]) do { let result = try api.echoNullable(aNullableListArg) @@ -788,7 +788,7 @@ class HostIntegrationCoreApiSetup { let echoNullableMapChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoNullableMap", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoNullableMapChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableMapArg: [String?: Any?]? = nilOrValue(args[0]) do { let result = try api.echoNullable(aNullableMapArg) @@ -821,7 +821,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncIntChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncInt", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncIntChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let anIntArg = args[0] is Int64 ? args[0] as! Int64 : Int64(args[0] as! Int32) api.echoAsync(anIntArg) { result in switch result { @@ -839,7 +839,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncDoubleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncDouble", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncDoubleChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aDoubleArg = args[0] as! Double api.echoAsync(aDoubleArg) { result in switch result { @@ -857,7 +857,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncBoolChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncBool", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncBoolChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aBoolArg = args[0] as! Bool api.echoAsync(aBoolArg) { result in switch result { @@ -875,7 +875,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aStringArg = args[0] as! String api.echoAsync(aStringArg) { result in switch result { @@ -893,7 +893,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncUint8ListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncUint8List", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncUint8ListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aUint8ListArg = args[0] as! FlutterStandardTypedData api.echoAsync(aUint8ListArg) { result in switch result { @@ -911,8 +911,8 @@ class HostIntegrationCoreApiSetup { let echoAsyncObjectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncObject", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncObjectChannel.setMessageHandler { message, reply in - let args = message as! [Any] - let anObjectArg = args[0] + let args = message as! [Any?] + let anObjectArg = args[0]! api.echoAsync(anObjectArg) { result in switch result { case .success(let res): @@ -929,7 +929,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncList", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aListArg = args[0] as! [Any?] api.echoAsync(aListArg) { result in switch result { @@ -947,7 +947,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncMapChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncMap", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncMapChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aMapArg = args[0] as! [String?: Any?] api.echoAsync(aMapArg) { result in switch result { @@ -1013,7 +1013,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncAllTypesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncAllTypes", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncAllTypesChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let everythingArg = args[0] as! AllTypes api.echoAsync(everythingArg) { result in switch result { @@ -1031,7 +1031,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableAllNullableTypesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableAllNullableTypes", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableAllNullableTypesChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let everythingArg: AllNullableTypes? = nilOrValue(args[0]) api.echoAsync(everythingArg) { result in switch result { @@ -1049,7 +1049,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableIntChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableInt", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableIntChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let anIntArg: Int64? = args[0] is NSNull ? nil : (args[0] is Int64? ? args[0] as! Int64? : Int64(args[0] as! Int32)) api.echoAsyncNullable(anIntArg) { result in switch result { @@ -1067,7 +1067,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableDoubleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableDouble", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableDoubleChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aDoubleArg: Double? = nilOrValue(args[0]) api.echoAsyncNullable(aDoubleArg) { result in switch result { @@ -1085,7 +1085,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableBoolChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableBool", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableBoolChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aBoolArg: Bool? = nilOrValue(args[0]) api.echoAsyncNullable(aBoolArg) { result in switch result { @@ -1103,7 +1103,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aStringArg: String? = nilOrValue(args[0]) api.echoAsyncNullable(aStringArg) { result in switch result { @@ -1121,7 +1121,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableUint8ListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableUint8List", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableUint8ListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aUint8ListArg: FlutterStandardTypedData? = nilOrValue(args[0]) api.echoAsyncNullable(aUint8ListArg) { result in switch result { @@ -1139,7 +1139,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableObjectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableObject", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableObjectChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let anObjectArg: Any? = args[0] api.echoAsyncNullable(anObjectArg) { result in switch result { @@ -1157,7 +1157,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableList", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aListArg: [Any?]? = nilOrValue(args[0]) api.echoAsyncNullable(aListArg) { result in switch result { @@ -1175,7 +1175,7 @@ class HostIntegrationCoreApiSetup { let echoAsyncNullableMapChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncNullableMap", binaryMessenger: binaryMessenger, codec: codec) if let api = api { echoAsyncNullableMapChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aMapArg: [String?: Any?]? = nilOrValue(args[0]) api.echAsyncoNullable(aMapArg) { result in switch result { @@ -1237,7 +1237,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoAllTypesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoAllTypes", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoAllTypesChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let everythingArg = args[0] as! AllTypes api.callFlutterEcho(everythingArg) { result in switch result { @@ -1254,7 +1254,7 @@ class HostIntegrationCoreApiSetup { let callFlutterSendMultipleNullableTypesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterSendMultipleNullableTypes", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterSendMultipleNullableTypesChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aNullableBoolArg: Bool? = nilOrValue(args[0]) let aNullableIntArg: Int64? = args[1] is NSNull ? nil : (args[1] is Int64? ? args[1] as! Int64? : Int64(args[1] as! Int32)) let aNullableStringArg: String? = nilOrValue(args[2]) @@ -1273,7 +1273,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoBoolChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoBool", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoBoolChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aBoolArg = args[0] as! Bool api.callFlutterEcho(aBoolArg) { result in switch result { @@ -1290,7 +1290,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoIntChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoInt", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoIntChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let anIntArg = args[0] is Int64 ? args[0] as! Int64 : Int64(args[0] as! Int32) api.callFlutterEcho(anIntArg) { result in switch result { @@ -1307,7 +1307,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoDoubleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoDouble", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoDoubleChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aDoubleArg = args[0] as! Double api.callFlutterEcho(aDoubleArg) { result in switch result { @@ -1324,7 +1324,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aStringArg = args[0] as! String api.callFlutterEcho(aStringArg) { result in switch result { @@ -1341,7 +1341,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoUint8ListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoUint8List", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoUint8ListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aListArg = args[0] as! FlutterStandardTypedData api.callFlutterEcho(aListArg) { result in switch result { @@ -1358,7 +1358,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoList", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aListArg = args[0] as! [Any?] api.callFlutterEcho(aListArg) { result in switch result { @@ -1375,7 +1375,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoMapChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoMap", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoMapChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aMapArg = args[0] as! [String?: Any?] api.callFlutterEcho(aMapArg) { result in switch result { @@ -1392,7 +1392,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoNullableBoolChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoNullableBool", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoNullableBoolChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aBoolArg: Bool? = nilOrValue(args[0]) api.callFlutterEchoNullable(aBoolArg) { result in switch result { @@ -1409,7 +1409,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoNullableIntChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoNullableInt", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoNullableIntChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let anIntArg: Int64? = args[0] is NSNull ? nil : (args[0] is Int64? ? args[0] as! Int64? : Int64(args[0] as! Int32)) api.callFlutterEchoNullable(anIntArg) { result in switch result { @@ -1426,7 +1426,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoNullableDoubleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoNullableDouble", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoNullableDoubleChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aDoubleArg: Double? = nilOrValue(args[0]) api.callFlutterEchoNullable(aDoubleArg) { result in switch result { @@ -1443,7 +1443,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoNullableStringChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoNullableString", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoNullableStringChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aStringArg: String? = nilOrValue(args[0]) api.callFlutterEchoNullable(aStringArg) { result in switch result { @@ -1460,7 +1460,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoNullableUint8ListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoNullableUint8List", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoNullableUint8ListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aListArg: FlutterStandardTypedData? = nilOrValue(args[0]) api.callFlutterEchoNullable(aListArg) { result in switch result { @@ -1477,7 +1477,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoNullableListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoNullableList", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoNullableListChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aListArg: [Any?]? = nilOrValue(args[0]) api.callFlutterEchoNullable(aListArg) { result in switch result { @@ -1494,7 +1494,7 @@ class HostIntegrationCoreApiSetup { let callFlutterEchoNullableMapChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoNullableMap", binaryMessenger: binaryMessenger, codec: codec) if let api = api { callFlutterEchoNullableMapChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aMapArg: [String?: Any?]? = nilOrValue(args[0]) api.callFlutterEchoNullable(aMapArg) { result in switch result { @@ -1514,13 +1514,13 @@ private class FlutterIntegrationCoreApiCodecReader: FlutterStandardReader { override func readValue(ofType type: UInt8) -> Any? { switch type { case 128: - return AllNullableTypes.fromList(self.readValue() as! [Any]) + return AllNullableTypes.fromList(self.readValue() as! [Any?]) case 129: - return AllNullableTypesWrapper.fromList(self.readValue() as! [Any]) + return AllNullableTypesWrapper.fromList(self.readValue() as! [Any?]) case 130: - return AllTypes.fromList(self.readValue() as! [Any]) + return AllTypes.fromList(self.readValue() as! [Any?]) case 131: - return TestMessage.fromList(self.readValue() as! [Any]) + return TestMessage.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) } @@ -1794,7 +1794,7 @@ class HostSmallApiSetup { let echoChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.HostSmallApi.echo", binaryMessenger: binaryMessenger) if let api = api { echoChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let aStringArg = args[0] as! String api.echo(aString: aStringArg) { result in switch result { @@ -1829,7 +1829,7 @@ private class FlutterSmallApiCodecReader: FlutterStandardReader { override func readValue(ofType type: UInt8) -> Any? { switch type { case 128: - return TestMessage.fromList(self.readValue() as! [Any]) + return TestMessage.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) } diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index c3fda037a749..26b6465413cd 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.5 # This must match the version in lib/generator_tools.dart +version: 10.0.0 # This must match the version in lib/generator_tools.dart environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/pigeon/test/swift_generator_test.dart b/packages/pigeon/test/swift_generator_test.dart index 452bf8f8424a..257cadbd56c9 100644 --- a/packages/pigeon/test/swift_generator_test.dart +++ b/packages/pigeon/test/swift_generator_test.dart @@ -31,7 +31,7 @@ void main() { final String code = sink.toString(); expect(code, contains('struct Foobar')); expect(code, contains('var field1: Int64? = nil')); - expect(code, contains('static func fromList(_ list: [Any]) -> Foobar?')); + expect(code, contains('static func fromList(_ list: [Any?]) -> Foobar?')); expect(code, contains('func toList() -> [Any?]')); }); @@ -392,7 +392,7 @@ void main() { generator.generate(swiftOptions, root, sink); final String code = sink.toString(); expect(code, contains('struct Foobar')); - expect(code, contains('var field1: [Any]? = nil')); + expect(code, contains('var field1: [Any?]? = nil')); }); test('gen map', () { @@ -412,7 +412,7 @@ void main() { generator.generate(swiftOptions, root, sink); final String code = sink.toString(); expect(code, contains('struct Foobar')); - expect(code, contains('var field1: [AnyHashable: Any]? = nil')); + expect(code, contains('var field1: [AnyHashable: Any?]? = nil')); }); test('gen nested', () { @@ -451,7 +451,7 @@ void main() { expect(code, contains('struct Outer')); expect(code, contains('struct Nested')); expect(code, contains('var nested: Nested? = nil')); - expect(code, contains('static func fromList(_ list: [Any]) -> Outer?')); + expect(code, contains('static func fromList(_ list: [Any?]) -> Outer?')); expect(code, contains('nested = Nested.fromList(nestedList)')); expect(code, contains('func toList() -> [Any?]')); }); @@ -796,7 +796,7 @@ void main() { generator.generate(swiftOptions, root, sink); final String code = sink.toString(); expect(code, contains('func add(x: Int64, y: Int64) throws -> Int64')); - expect(code, contains('let args = message as! [Any]')); + expect(code, contains('let args = message as! [Any?]')); expect( code, contains( diff --git a/packages/pointer_interceptor/CHANGELOG.md b/packages/pointer_interceptor/CHANGELOG.md index 368fd6d6caca..a9a8b55f1d08 100644 --- a/packages/pointer_interceptor/CHANGELOG.md +++ b/packages/pointer_interceptor/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. * Aligns Dart and Flutter SDK constraints. ## 0.9.3+4 diff --git a/packages/pointer_interceptor/example/pubspec.yaml b/packages/pointer_interceptor/example/pubspec.yaml index bfbd24228d12..3a76753e527b 100644 --- a/packages/pointer_interceptor/example/pubspec.yaml +++ b/packages/pointer_interceptor/example/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: 'none' version: 1.0.0 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/pointer_interceptor/pubspec.yaml b/packages/pointer_interceptor/pubspec.yaml index a8a451da84d8..22106a80db7c 100644 --- a/packages/pointer_interceptor/pubspec.yaml +++ b/packages/pointer_interceptor/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 0.9.3+4 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md index 10913d1b2113..4c5a3cec9967 100644 --- a/packages/quick_actions/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -1,5 +1,10 @@ -## NEXT +## 1.0.4 +* Removes obsolete null checks on non-nullable values. + +## 1.0.3 + +* Updates iOS minimum version in README. * Updates minimum Flutter version to 3.3. * Aligns Dart and Flutter SDK constraints. diff --git a/packages/quick_actions/quick_actions/example/.pluginToolsConfig.yaml b/packages/quick_actions/quick_actions/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/quick_actions/quick_actions/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/quick_actions/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist b/packages/quick_actions/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f96d4..9b41e7d87980 100644 --- a/packages/quick_actions/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/quick_actions/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/quick_actions/quick_actions/example/ios/Podfile b/packages/quick_actions/quick_actions/example/ios/Podfile index 3924e59aa0f9..ec43b513b0d1 100644 --- a/packages/quick_actions/quick_actions/example/ios/Podfile +++ b/packages/quick_actions/quick_actions/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj index d6cb74d0658b..67c49eb4e523 100644 --- a/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -269,7 +269,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 33E20B3126EFCDFC00A4A191 = { @@ -337,6 +337,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -373,6 +374,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -505,7 +507,7 @@ CODE_SIGN_STYLE = Automatic; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -528,7 +530,7 @@ CODE_SIGN_STYLE = Automatic; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; @@ -586,7 +588,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -636,7 +638,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index ac798eda8c17..1ba2b47c79f1 100644 --- a/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/quick_actions/quick_actions/example/lib/main.dart b/packages/quick_actions/quick_actions/example/lib/main.dart index 759f7f132158..14429284c621 100644 --- a/packages/quick_actions/quick_actions/example/lib/main.dart +++ b/packages/quick_actions/quick_actions/example/lib/main.dart @@ -43,9 +43,7 @@ class _MyHomePageState extends State { const QuickActions quickActions = QuickActions(); quickActions.initialize((String shortcutType) { setState(() { - if (shortcutType != null) { - shortcut = shortcutType; - } + shortcut = shortcutType; }); }); diff --git a/packages/quick_actions/quick_actions/pubspec.yaml b/packages/quick_actions/quick_actions/pubspec.yaml index 710a03eb4118..5fea6a036be1 100644 --- a/packages/quick_actions/quick_actions/pubspec.yaml +++ b/packages/quick_actions/quick_actions/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for creating shortcuts on home screen, also known as Quick Actions on iOS and App Shortcuts on Android. repository: https://github.com/flutter/packages/tree/main/packages/quick_actions/quick_actions issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+quick_actions%22 -version: 1.0.2 +version: 1.0.4 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/quick_actions/quick_actions_android/CHANGELOG.md b/packages/quick_actions/quick_actions_android/CHANGELOG.md index 32a0fa16d40b..c1a07dcda768 100644 --- a/packages/quick_actions/quick_actions_android/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_android/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.0.6 + +* Removes obsolete null checks on non-nullable values. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 1.0.5 + +* Fixes Java warnings. + ## 1.0.4 * Fixes compatibility with AGP versions older than 4.2. diff --git a/packages/quick_actions/quick_actions_android/android/build.gradle b/packages/quick_actions/quick_actions_android/android/build.gradle index 71bd5b2d6d21..942584d964d8 100644 --- a/packages/quick_actions/quick_actions_android/android/build.gradle +++ b/packages/quick_actions/quick_actions_android/android/build.gradle @@ -36,7 +36,6 @@ android { checkAllWarnings true warningsAsErrors true disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' - baseline file("lint-baseline.xml") } dependencies { diff --git a/packages/quick_actions/quick_actions_android/android/lint-baseline.xml b/packages/quick_actions/quick_actions_android/android/lint-baseline.xml deleted file mode 100644 index e0ecdc88cdeb..000000000000 --- a/packages/quick_actions/quick_actions_android/android/lint-baseline.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java index 96b141fb9c31..c6a0db5d0b1a 100644 --- a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java +++ b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java @@ -15,11 +15,13 @@ import android.os.Build; import android.os.Handler; import android.os.Looper; +import androidx.annotation.NonNull; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -27,7 +29,6 @@ class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { protected static final String EXTRA_ACTION = "some unique action key"; - private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions_android"; private final Context context; private Activity activity; @@ -42,7 +43,7 @@ void setActivity(Activity activity) { } @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { // We already know that this functionality does not work for anything // lower than API 25 so we chose not to return error. Instead we do nothing. @@ -54,13 +55,12 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { switch (call.method) { case "setShortcutItems": if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { - List> serializedShortcuts = call.arguments(); + List> serializedShortcuts = Objects.requireNonNull(call.arguments()); List shortcuts = deserializeShortcuts(serializedShortcuts); Executor uiThreadExecutor = new UiThreadExecutor(); ThreadPoolExecutor executor = - new ThreadPoolExecutor( - 0, 1, 1, TimeUnit.SECONDS, new LinkedBlockingQueue()); + new ThreadPoolExecutor(0, 1, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); executor.execute( () -> { @@ -140,6 +140,8 @@ private List deserializeShortcuts(List> shortc return shortcutInfos; } + // This method requires doing dynamic resource lookup, which is a discouraged API. + @SuppressWarnings("DiscouragedApi") private int loadResourceId(Context context, String icon) { if (icon == null) { return 0; @@ -167,7 +169,7 @@ private Intent getIntentToOpenMainActivity(String type) { .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); } - private static class UiThreadExecutor implements Executor { + static class UiThreadExecutor implements Executor { private final Handler handler = new Handler(Looper.getMainLooper()); @Override diff --git a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java index b41087816889..91950f3d7992 100644 --- a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java +++ b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java @@ -9,6 +9,7 @@ import android.content.Intent; import android.content.pm.ShortcutManager; import android.os.Build; +import androidx.annotation.NonNull; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; @@ -30,23 +31,24 @@ public class QuickActionsPlugin implements FlutterPlugin, ActivityAware, NewInte *

Must be called when the application is created. */ @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) { final QuickActionsPlugin plugin = new QuickActionsPlugin(); plugin.setupChannel(registrar.messenger(), registrar.context(), registrar.activity()); } @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { setupChannel(binding.getBinaryMessenger(), binding.getApplicationContext(), null); } @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { teardownChannel(); } @Override - public void onAttachedToActivity(ActivityPluginBinding binding) { + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { activity = binding.getActivity(); handler.setActivity(activity); binding.addOnNewIntentListener(this); @@ -59,7 +61,7 @@ public void onDetachedFromActivity() { } @Override - public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { binding.removeOnNewIntentListener(this); onAttachedToActivity(binding); } @@ -70,7 +72,7 @@ public void onDetachedFromActivityForConfigChanges() { } @Override - public boolean onNewIntent(Intent intent) { + public boolean onNewIntent(@NonNull Intent intent) { // Do nothing for anything lower than API 25 as the functionality isn't supported. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { return false; diff --git a/packages/quick_actions/quick_actions_android/example/.pluginToolsConfig.yaml b/packages/quick_actions/quick_actions_android/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/quick_actions/quick_actions_android/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/quick_actions/quick_actions_android/example/lib/main.dart b/packages/quick_actions/quick_actions_android/example/lib/main.dart index e39e6afef539..a0352c8e482e 100644 --- a/packages/quick_actions/quick_actions_android/example/lib/main.dart +++ b/packages/quick_actions/quick_actions_android/example/lib/main.dart @@ -43,9 +43,7 @@ class _MyHomePageState extends State { final QuickActionsAndroid quickActions = QuickActionsAndroid(); quickActions.initialize((String shortcutType) { setState(() { - if (shortcutType != null) { - shortcut = '$shortcutType has launched'; - } + shortcut = '$shortcutType has launched'; }); }); diff --git a/packages/quick_actions/quick_actions_android/example/pubspec.yaml b/packages/quick_actions/quick_actions_android/example/pubspec.yaml index 987df0b9caac..cffc4415d974 100644 --- a/packages/quick_actions/quick_actions_android/example/pubspec.yaml +++ b/packages/quick_actions/quick_actions_android/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the quick_actions plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/quick_actions/quick_actions_android/pubspec.yaml b/packages/quick_actions/quick_actions_android/pubspec.yaml index 65f71d6b157d..04884c40e4a5 100644 --- a/packages/quick_actions/quick_actions_android/pubspec.yaml +++ b/packages/quick_actions/quick_actions_android/pubspec.yaml @@ -2,11 +2,11 @@ name: quick_actions_android description: An implementation for the Android platform of the Flutter `quick_actions` plugin. repository: https://github.com/flutter/packages/tree/main/packages/quick_actions/quick_actions_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 1.0.4 +version: 1.0.6 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/quick_actions/quick_actions_ios/CHANGELOG.md b/packages/quick_actions/quick_actions_ios/CHANGELOG.md index 1a999f89d2b2..205e1ce43c21 100644 --- a/packages/quick_actions/quick_actions_ios/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_ios/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.0.6 + +* Removes obsolete null checks on non-nullable values. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 1.0.5 + +* Updates minimum iOS version to 11 and Flutter version to 3.3. + ## 1.0.4 * Clarifies explanation of endorsement in README. @@ -15,7 +24,7 @@ ## 1.0.1 -* Removes custom modulemap file with "Test" submodule and private headers for Swift migration. +* Removes custom modulemap file with "Test" submodule and private headers for Swift migration. * Migrates `FLTQuickActionsPlugin` class to Swift. ## 1.0.0 diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/quick_actions/quick_actions_ios/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f96d4..9b41e7d87980 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/quick_actions/quick_actions_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Podfile b/packages/quick_actions/quick_actions_ios/example/ios/Podfile index 3924e59aa0f9..ec43b513b0d1 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Podfile +++ b/packages/quick_actions/quick_actions_ios/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj index f5b708bbb54b..395847ae2de1 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -547,7 +547,7 @@ CODE_SIGN_STYLE = Automatic; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -577,7 +577,7 @@ CODE_SIGN_STYLE = Automatic; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -640,7 +640,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -690,7 +690,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/quick_actions/quick_actions_ios/example/lib/main.dart b/packages/quick_actions/quick_actions_ios/example/lib/main.dart index f2c8d61b8def..f3e9f92b2cbb 100644 --- a/packages/quick_actions/quick_actions_ios/example/lib/main.dart +++ b/packages/quick_actions/quick_actions_ios/example/lib/main.dart @@ -43,9 +43,7 @@ class _MyHomePageState extends State { final QuickActionsIos quickActions = QuickActionsIos(); quickActions.initialize((String shortcutType) { setState(() { - if (shortcutType != null) { - shortcut = shortcutType; - } + shortcut = shortcutType; }); }); diff --git a/packages/quick_actions/quick_actions_ios/example/pubspec.yaml b/packages/quick_actions/quick_actions_ios/example/pubspec.yaml index 5f38af92df10..10fc84961374 100644 --- a/packages/quick_actions/quick_actions_ios/example/pubspec.yaml +++ b/packages/quick_actions/quick_actions_ios/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the quick_actions plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios.podspec b/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios.podspec index f247b02bef68..466aa0dffc49 100644 --- a/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios.podspec +++ b/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios.podspec @@ -21,6 +21,6 @@ Downloaded by pub (not CocoaPods). 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', } s.dependency 'Flutter' - s.platform = :ios, '9.0' + s.platform = :ios, '11.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/quick_actions/quick_actions_ios/pubspec.yaml b/packages/quick_actions/quick_actions_ios/pubspec.yaml index 7d5bd533b04a..6800f01ac221 100644 --- a/packages/quick_actions/quick_actions_ios/pubspec.yaml +++ b/packages/quick_actions/quick_actions_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: quick_actions_ios description: An implementation for the iOS platform of the Flutter `quick_actions` plugin. repository: https://github.com/flutter/packages/tree/main/packages/quick_actions/quick_actions_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 1.0.4 +version: 1.0.6 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/rfw/CHANGELOG.md b/packages/rfw/CHANGELOG.md index 21e198516c55..2a6e3a4e12bd 100644 --- a/packages/rfw/CHANGELOG.md +++ b/packages/rfw/CHANGELOG.md @@ -1,6 +1,8 @@ -## NEXT +## 1.0.9 +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. * Aligns Dart and Flutter SDK constraints. +* Fixes a typo in the API documentation that broke the formatting. ## 1.0.8 @@ -9,7 +11,7 @@ ## 1.0.7 -* Update README. +* Updates README. ## 1.0.6 @@ -34,15 +36,15 @@ ## 1.0.2 -* Mention FractionallySizedBox in documentation. +* Mentions FractionallySizedBox in documentation. ## 1.0.1 -* Improve documentation. -* Provide constants for the file signatures. +* Improves documentation. +* Provides constants for the file signatures. * Minor efficiency improvements. -* Fix `unnecessary_import` lint errors. -* Add one more core widget, FractionallySizedBox. +* Fixes `unnecessary_import` lint errors. +* Adds one more core widget, FractionallySizedBox. ## 1.0.0 diff --git a/packages/rfw/example/hello/android/build.gradle b/packages/rfw/example/hello/android/build.gradle index ff27ef813e94..586557f0afcf 100644 --- a/packages/rfw/example/hello/android/build.gradle +++ b/packages/rfw/example/hello/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.0' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() diff --git a/packages/rfw/example/hello/ios/Flutter/AppFrameworkInfo.plist b/packages/rfw/example/hello/ios/Flutter/AppFrameworkInfo.plist index 8d4492f977ad..9625e105df39 100644 --- a/packages/rfw/example/hello/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/rfw/example/hello/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/rfw/example/hello/ios/Runner.xcodeproj/project.pbxproj b/packages/rfw/example/hello/ios/Runner.xcodeproj/project.pbxproj index e77cf432aacd..4d4076ef1d5c 100644 --- a/packages/rfw/example/hello/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/rfw/example/hello/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -171,6 +171,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -185,6 +186,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -272,7 +274,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -349,7 +351,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -398,7 +400,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/rfw/example/hello/ios/Runner/Info.plist b/packages/rfw/example/hello/ios/Runner/Info.plist index cd36e6d138cf..2f6dda1e835f 100644 --- a/packages/rfw/example/hello/ios/Runner/Info.plist +++ b/packages/rfw/example/hello/ios/Runner/Info.plist @@ -41,5 +41,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/rfw/example/hello/macos/Runner.xcodeproj/project.pbxproj b/packages/rfw/example/hello/macos/Runner.xcodeproj/project.pbxproj index e3be73975db3..0035a6032d4e 100644 --- a/packages/rfw/example/hello/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/rfw/example/hello/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -235,6 +235,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -344,7 +345,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -423,7 +424,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -470,7 +471,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/rfw/example/hello/pubspec.yaml b/packages/rfw/example/hello/pubspec.yaml index aac9a311ff34..6d720d2468a6 100644 --- a/packages/rfw/example/hello/pubspec.yaml +++ b/packages/rfw/example/hello/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/rfw/example/local/android/build.gradle b/packages/rfw/example/local/android/build.gradle index ff27ef813e94..586557f0afcf 100644 --- a/packages/rfw/example/local/android/build.gradle +++ b/packages/rfw/example/local/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.0' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() diff --git a/packages/rfw/example/local/ios/Flutter/AppFrameworkInfo.plist b/packages/rfw/example/local/ios/Flutter/AppFrameworkInfo.plist index 8d4492f977ad..9625e105df39 100644 --- a/packages/rfw/example/local/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/rfw/example/local/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/rfw/example/local/ios/Runner.xcodeproj/project.pbxproj b/packages/rfw/example/local/ios/Runner.xcodeproj/project.pbxproj index d231a20e8ef3..532f29540351 100644 --- a/packages/rfw/example/local/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/rfw/example/local/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -171,6 +171,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -185,6 +186,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -272,7 +274,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -349,7 +351,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -398,7 +400,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/rfw/example/local/ios/Runner/Info.plist b/packages/rfw/example/local/ios/Runner/Info.plist index 2150ab0e17b0..031ce64cd1e8 100644 --- a/packages/rfw/example/local/ios/Runner/Info.plist +++ b/packages/rfw/example/local/ios/Runner/Info.plist @@ -41,5 +41,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/rfw/example/local/macos/Runner.xcodeproj/project.pbxproj b/packages/rfw/example/local/macos/Runner.xcodeproj/project.pbxproj index 43e5e2e6185a..7dd14be13fb2 100644 --- a/packages/rfw/example/local/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/rfw/example/local/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -235,6 +235,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -344,7 +345,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -423,7 +424,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -470,7 +471,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/rfw/example/local/pubspec.yaml b/packages/rfw/example/local/pubspec.yaml index b81b031b329c..620338a653a5 100644 --- a/packages/rfw/example/local/pubspec.yaml +++ b/packages/rfw/example/local/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/rfw/example/remote/android/build.gradle b/packages/rfw/example/remote/android/build.gradle index ff27ef813e94..586557f0afcf 100644 --- a/packages/rfw/example/remote/android/build.gradle +++ b/packages/rfw/example/remote/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.0' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() diff --git a/packages/rfw/example/remote/ios/Flutter/AppFrameworkInfo.plist b/packages/rfw/example/remote/ios/Flutter/AppFrameworkInfo.plist index 8d4492f977ad..9625e105df39 100644 --- a/packages/rfw/example/remote/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/rfw/example/remote/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/rfw/example/remote/ios/Runner.xcodeproj/project.pbxproj b/packages/rfw/example/remote/ios/Runner.xcodeproj/project.pbxproj index 758c2d361b07..4d4ed5b5e790 100644 --- a/packages/rfw/example/remote/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/rfw/example/remote/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -13,6 +13,7 @@ 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 */; }; + 9CE78CAA22092B01A34858F6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7C0A1BEE4027C17F876680E7 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -32,9 +33,12 @@ 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 = ""; }; + 53C2E067362F97105BC29D1E /* 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 = ""; }; 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 = ""; }; + 7C0A1BEE4027C17F876680E7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 930017DBB28105284FF6FFC7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -42,6 +46,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + FC32B7ECAEDCADC107987DDF /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +54,32 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 9CE78CAA22092B01A34858F6 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 33229402E4F143A40B98C88E /* Pods */ = { + isa = PBXGroup; + children = ( + FC32B7ECAEDCADC107987DDF /* Pods-Runner.debug.xcconfig */, + 53C2E067362F97105BC29D1E /* Pods-Runner.release.xcconfig */, + 930017DBB28105284FF6FFC7 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 8D7A9CCBDDED29EDF28765FC /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7C0A1BEE4027C17F876680E7 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +97,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 33229402E4F143A40B98C88E /* Pods */, + 8D7A9CCBDDED29EDF28765FC /* Frameworks */, ); sourceTree = ""; }; @@ -105,12 +132,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 850FDD5F197CBC384057B95E /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 1935C22EEC3855A500751B6C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -169,8 +198,26 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 1935C22EEC3855A500751B6C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -183,8 +230,31 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 850FDD5F197CBC384057B95E /* [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; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -272,7 +342,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -349,7 +419,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -398,7 +468,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/rfw/example/remote/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/rfw/example/remote/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..21a3cc14c74e 100644 --- a/packages/rfw/example/remote/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/packages/rfw/example/remote/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/packages/rfw/example/remote/ios/Runner/Info.plist b/packages/rfw/example/remote/ios/Runner/Info.plist index dcfd841a4167..c11cb1e95dc8 100644 --- a/packages/rfw/example/remote/ios/Runner/Info.plist +++ b/packages/rfw/example/remote/ios/Runner/Info.plist @@ -41,5 +41,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/rfw/example/remote/macos/Podfile b/packages/rfw/example/remote/macos/Podfile index dade8dfad0dc..049abe295427 100644 --- a/packages/rfw/example/remote/macos/Podfile +++ b/packages/rfw/example/remote/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/rfw/example/remote/macos/Runner.xcodeproj/project.pbxproj b/packages/rfw/example/remote/macos/Runner.xcodeproj/project.pbxproj index 2f5eaa9dafa8..86db0ae95714 100644 --- a/packages/rfw/example/remote/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/rfw/example/remote/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -21,6 +21,7 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 02C0D91643FE6F39BEA2BB6E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 843B05D923F2BC6B7E429B6A /* Pods_Runner.framework */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; @@ -54,7 +55,7 @@ /* Begin PBXFileReference section */ 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 = ""; }; - 33CC10ED2044A3C60003C045 /* remote.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "remote.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* remote.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = remote.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 = ""; }; @@ -67,7 +68,11 @@ 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 843B05D923F2BC6B7E429B6A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + B3379FCDBBE6204BEF2B5C16 /* 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 = ""; }; + CD4ACEFE55709DCCDCD58135 /* 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 = ""; }; + DF79506D30CEA9FF7E0A2EF7 /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -75,6 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 02C0D91643FE6F39BEA2BB6E /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,6 +105,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 86AA60D5EB1EA06D4F24A876 /* Pods */, ); sourceTree = ""; }; @@ -145,9 +152,21 @@ path = Runner; sourceTree = ""; }; + 86AA60D5EB1EA06D4F24A876 /* Pods */ = { + isa = PBXGroup; + children = ( + B3379FCDBBE6204BEF2B5C16 /* Pods-Runner.debug.xcconfig */, + CD4ACEFE55709DCCDCD58135 /* Pods-Runner.release.xcconfig */, + DF79506D30CEA9FF7E0A2EF7 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 843B05D923F2BC6B7E429B6A /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -159,11 +178,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 3A6B5419711B5B56FAA0549E /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 286D1BAFE1D2A0B1415EA0DA /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -233,8 +254,26 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 286D1BAFE1D2A0B1415EA0DA /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -270,6 +309,28 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 3A6B5419711B5B56FAA0549E /* [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 */ @@ -344,7 +405,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -423,7 +484,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -470,7 +531,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/rfw/example/remote/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/rfw/example/remote/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..21a3cc14c74e 100644 --- a/packages/rfw/example/remote/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/packages/rfw/example/remote/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/packages/rfw/example/remote/pubspec.yaml b/packages/rfw/example/remote/pubspec.yaml index cb2c4bdf945d..0b744cc987a3 100644 --- a/packages/rfw/example/remote/pubspec.yaml +++ b/packages/rfw/example/remote/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/rfw/example/wasm/macos/Podfile b/packages/rfw/example/wasm/macos/Podfile index dade8dfad0dc..049abe295427 100644 --- a/packages/rfw/example/wasm/macos/Podfile +++ b/packages/rfw/example/wasm/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/rfw/example/wasm/macos/Runner.xcodeproj/project.pbxproj b/packages/rfw/example/wasm/macos/Runner.xcodeproj/project.pbxproj index 19d957af3c3c..122b68e4b99a 100644 --- a/packages/rfw/example/wasm/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/rfw/example/wasm/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -26,6 +26,7 @@ 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 */; }; + AA805F536C403E9DF2F216F0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3DFADD57E822077DC785F2E /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -52,9 +53,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 11AA1FECB62824C88FCB378B /* 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 = ""; }; 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 = ""; }; - 33CC10ED2044A3C60003C045 /* wasm.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "wasm.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* wasm.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = wasm.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 = ""; }; @@ -68,6 +70,9 @@ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.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 = ""; }; + B81230EC0D64B72D1F46B696 /* 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 = ""; }; + BDCFE4DB06DC4D61967F2F6B /* 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 = ""; }; + C3DFADD57E822077DC785F2E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -75,6 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + AA805F536C403E9DF2F216F0 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,6 +105,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + DB7DE30525E219C950F9CD67 /* Pods */, ); sourceTree = ""; }; @@ -148,10 +155,22 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + C3DFADD57E822077DC785F2E /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; + DB7DE30525E219C950F9CD67 /* Pods */ = { + isa = PBXGroup; + children = ( + B81230EC0D64B72D1F46B696 /* Pods-Runner.debug.xcconfig */, + BDCFE4DB06DC4D61967F2F6B /* Pods-Runner.release.xcconfig */, + 11AA1FECB62824C88FCB378B /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -159,11 +178,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 1ABA20DC34222FFC08F176B7 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 0623D74E96A0E983167ABA73 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -233,8 +254,48 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 0623D74E96A0E983167ABA73 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 1ABA20DC34222FFC08F176B7 /* [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; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -344,7 +405,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -423,7 +484,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -470,7 +531,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/rfw/example/wasm/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/rfw/example/wasm/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..21a3cc14c74e 100644 --- a/packages/rfw/example/wasm/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/packages/rfw/example/wasm/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/packages/rfw/example/wasm/pubspec.yaml b/packages/rfw/example/wasm/pubspec.yaml index e72fcfd26898..84fa83b971f5 100644 --- a/packages/rfw/example/wasm/pubspec.yaml +++ b/packages/rfw/example/wasm/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: none # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/rfw/lib/src/dart/text.dart b/packages/rfw/lib/src/dart/text.dart index edd490d31bef..9d089b2a8508 100644 --- a/packages/rfw/lib/src/dart/text.dart +++ b/packages/rfw/lib/src/dart/text.dart @@ -415,7 +415,6 @@ DynamicMap parseDataFile(String file) { /// The syntax for a loop uses the following form: /// /// ...for ident in list: template -/// ``` /// /// ...where _ident_ is the identifier to bind to each value in the list, _list_ /// is some value that evaluates to a list, and _template_ is a value that is to diff --git a/packages/rfw/pubspec.yaml b/packages/rfw/pubspec.yaml index 47cbe113fac3..5db6474642d1 100644 --- a/packages/rfw/pubspec.yaml +++ b/packages/rfw/pubspec.yaml @@ -2,11 +2,11 @@ name: rfw description: "Remote Flutter widgets: a library for rendering declarative widget description files at runtime." repository: https://github.com/flutter/packages/tree/main/packages/rfw issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+rfw%22 -version: 1.0.8 +version: 1.0.9 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/rfw/test_coverage/pubspec.yaml b/packages/rfw/test_coverage/pubspec.yaml index 908290b50244..a604b39f43ac 100644 --- a/packages/rfw/test_coverage/pubspec.yaml +++ b/packages/rfw/test_coverage/pubspec.yaml @@ -4,7 +4,7 @@ version: 1.0.0 publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=2.18.0 <4.0.0" dependencies: lcov_parser: 0.1.1 diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 7ee98815988f..3ee0cabefd7a 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Updates minimum supported macOS version to 10.14. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 2.1.1 + +* Updates iOS minimum version in README. + ## 2.1.0 * Adds `setPrefix` method. diff --git a/packages/shared_preferences/shared_preferences/README.md b/packages/shared_preferences/shared_preferences/README.md index e06c9fd6e8f5..527db6e8fe3a 100644 --- a/packages/shared_preferences/shared_preferences/README.md +++ b/packages/shared_preferences/shared_preferences/README.md @@ -11,9 +11,9 @@ returning, so this plugin must not be used for storing critical data. Supported data types are `int`, `double`, `bool`, `String` and `List`. -| | Android | iOS | Linux | macOS | Web | Windows | -|-------------|---------|------|-------|--------|-----|-------------| -| **Support** | SDK 16+ | 9.0+ | Any | 10.11+ | Any | Any | +| | Android | iOS | Linux | macOS | Web | Windows | +|-------------|---------|-------|-------|--------|-----|-------------| +| **Support** | SDK 16+ | 11.0+ | Any | 10.14+ | Any | Any | ## Usage To use this plugin, add `shared_preferences` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). diff --git a/packages/shared_preferences/shared_preferences/example/.pluginToolsConfig.yaml b/packages/shared_preferences/shared_preferences/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/shared_preferences/shared_preferences/example/android/build.gradle b/packages/shared_preferences/shared_preferences/example/android/build.gradle index 4b30292ebe1f..ce647a433bd0 100644 --- a/packages/shared_preferences/shared_preferences/example/android/build.gradle +++ b/packages/shared_preferences/shared_preferences/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.21' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() diff --git a/packages/shared_preferences/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist b/packages/shared_preferences/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f96d4..9b41e7d87980 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/shared_preferences/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/shared_preferences/shared_preferences/example/ios/Podfile b/packages/shared_preferences/shared_preferences/example/ios/Podfile index f7d6a5e68c3a..d207307f86d7 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Podfile +++ b/packages/shared_preferences/shared_preferences/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj index 5040eae278b8..ebf474697b10 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -169,7 +169,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -212,6 +212,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -226,6 +227,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -339,7 +341,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -389,7 +391,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e29b432c48c..11638dc42e95 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/shared_preferences/shared_preferences/example/linux/flutter/CMakeLists.txt b/packages/shared_preferences/shared_preferences/example/linux/flutter/CMakeLists.txt index 4f48a7ced5f4..94f43ff7fa6a 100644 --- a/packages/shared_preferences/shared_preferences/example/linux/flutter/CMakeLists.txt +++ b/packages/shared_preferences/shared_preferences/example/linux/flutter/CMakeLists.txt @@ -24,7 +24,6 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) -pkg_check_modules(BLKID REQUIRED IMPORTED_TARGET blkid) set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") @@ -66,7 +65,6 @@ target_link_libraries(flutter INTERFACE PkgConfig::GTK PkgConfig::GLIB PkgConfig::GIO - PkgConfig::BLKID ) add_dependencies(flutter flutter_assemble) diff --git a/packages/shared_preferences/shared_preferences/example/macos/Podfile b/packages/shared_preferences/shared_preferences/example/macos/Podfile index dade8dfad0dc..049abe295427 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Podfile +++ b/packages/shared_preferences/shared_preferences/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.pbxproj index cc89c8782812..ae863ebd6924 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -26,6 +26,7 @@ 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 */; }; + DA98078CB4612E9A884E9EE1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 688249AA8BCD9890EFC6A3C3 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -54,7 +55,7 @@ /* Begin PBXFileReference section */ 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 = ""; }; - 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.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 = ""; }; @@ -66,8 +67,12 @@ 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 = ""; }; + 688249AA8BCD9890EFC6A3C3 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 8BE07209243A880815CD0655 /* 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 = ""; }; + 9559BFA07E131693F36756D1 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + FB517C88258DF7E2701C5EBD /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -75,6 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DA98078CB4612E9A884E9EE1 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,6 +105,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + F58EF12F35E2D6D991BD099C /* Pods */, ); sourceTree = ""; }; @@ -148,10 +155,22 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 688249AA8BCD9890EFC6A3C3 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; + F58EF12F35E2D6D991BD099C /* Pods */ = { + isa = PBXGroup; + children = ( + FB517C88258DF7E2701C5EBD /* Pods-Runner.debug.xcconfig */, + 8BE07209243A880815CD0655 /* Pods-Runner.release.xcconfig */, + 9559BFA07E131693F36756D1 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -159,11 +178,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 8C5B8601E60D36BDBCF524F3 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 06AFE12182F28F3D8234B753 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -182,7 +203,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -233,8 +254,26 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 06AFE12182F28F3D8234B753 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -270,6 +309,28 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 8C5B8601E60D36BDBCF524F3 /* [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 */ @@ -344,7 +405,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -423,7 +484,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -470,7 +531,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index ae8ff59d97b3..7fd7126b0281 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + + diff --git a/packages/shared_preferences/shared_preferences/example/pubspec.yaml b/packages/shared_preferences/shared_preferences/example/pubspec.yaml index a421ca0cf4b4..7523d8201e98 100644 --- a/packages/shared_preferences/shared_preferences/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the shared_preferences plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index 2b1acaaf3948..02335fe2e7aa 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.1.0 +version: 2.1.1 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md index e901898e9421..e5dd9bcd5c33 100644 --- a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.1.4 * Fixes compatibility with AGP versions older than 4.2. diff --git a/packages/shared_preferences/shared_preferences_android/example/.pluginToolsConfig.yaml b/packages/shared_preferences/shared_preferences_android/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" 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 c11ef06c4e3b..f6f6475fb39e 100644 --- a/packages/shared_preferences/shared_preferences_android/example/android/build.gradle +++ b/packages/shared_preferences/shared_preferences_android/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.21' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() diff --git a/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml index 663e22959a25..34453450636a 100644 --- a/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the shared_preferences plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_android/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/pubspec.yaml index b8d240a78265..36fc4f41d842 100644 --- a/packages/shared_preferences/shared_preferences_android/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_android/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.1.4 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md b/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md index 78d862f614b7..6d7777da900b 100644 --- a/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Updates minimum supported macOS version to 10.14. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 2.2.2 + +* Updates minimum iOS version to 11. + ## 2.2.1 * Updates pigeon for null value handling fixes. diff --git a/packages/shared_preferences/shared_preferences_foundation/darwin/shared_preferences_foundation.podspec b/packages/shared_preferences/shared_preferences_foundation/darwin/shared_preferences_foundation.podspec index 5ad2d12caee7..3fae2e1ea060 100644 --- a/packages/shared_preferences/shared_preferences_foundation/darwin/shared_preferences_foundation.podspec +++ b/packages/shared_preferences/shared_preferences_foundation/darwin/shared_preferences_foundation.podspec @@ -15,8 +15,8 @@ Wraps NSUserDefaults, providing a persistent store for simple key-value pairs. s.source_files = 'Classes/**/*' s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' - s.ios.deployment_target = '9.0' - s.osx.deployment_target = '10.11' + s.ios.deployment_target = '11.0' + s.osx.deployment_target = '10.14' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.xcconfig = { 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', diff --git a/packages/shared_preferences/shared_preferences_foundation/example/macos/Podfile b/packages/shared_preferences/shared_preferences_foundation/example/macos/Podfile index e8da8332969a..47c1b18fedae 100644 --- a/packages/shared_preferences/shared_preferences_foundation/example/macos/Podfile +++ b/packages/shared_preferences/shared_preferences_foundation/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcodeproj/project.pbxproj index 0bfa5f0a93d7..d66a37745040 100644 --- a/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcodeproj/project.pbxproj @@ -510,7 +510,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -593,7 +593,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -640,7 +640,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/shared_preferences/shared_preferences_foundation/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_foundation/example/pubspec.yaml index c07881a7d315..685231d222a9 100644 --- a/packages/shared_preferences/shared_preferences_foundation/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_foundation/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Testbed for the shared_preferences_foundation implementation. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_foundation/pubspec.yaml b/packages/shared_preferences/shared_preferences_foundation/pubspec.yaml index ba453d098684..d2ee5cb68675 100644 --- a/packages/shared_preferences/shared_preferences_foundation/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_foundation/pubspec.yaml @@ -2,11 +2,11 @@ name: shared_preferences_foundation description: iOS and macOS implementation of the shared_preferences plugin. repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_foundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.2.1 +version: 2.2.2 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md index 4ba0cec9781f..be38e8420af5 100644 --- a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.2.0 * Adds `getAllWithPrefix` and `clearWithPrefix` methods. diff --git a/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml index b077ea13c9ca..d67cd6986580 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the shared_preferences_linux plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml index ff3f6d2b92ba..59ca5c4dfa62 100644 --- a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.2.0 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md index 5f29cb75b63f..bf59e87f4fc5 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.2.0 * Adds `getAllWithPrefix` and `clearWithPrefix` method. diff --git a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml index 15e5e59c1c35..76164d5d249d 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.2.0 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md index b5efabce677c..a446c0c721ff 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.1.0 * Adds `getAllWithPrefix` and `clearWithPrefix` methods. diff --git a/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml index 1afeef8c3109..92ace9c6d720 100644 --- a/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml @@ -2,8 +2,8 @@ name: shared_preferences_web_integration_tests publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_web/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/pubspec.yaml index 5c88356b3ef7..f7f7903f8ac9 100644 --- a/packages/shared_preferences/shared_preferences_web/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.1.0 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md index caeabe4e04c8..b7c641027717 100644 --- a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.2.0 * Adds `getAllWithPrefix` and `clearWithPrefix` methods. diff --git a/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml index cadf57852ce0..f0280cb4d2fb 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the shared_preferences_windows plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml index ae2246b506ca..03e3880b4e5d 100644 --- a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.2.0 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/standard_message_codec/CHANGELOG.md b/packages/standard_message_codec/CHANGELOG.md index 5472fdff1734..8d0035626e54 100644 --- a/packages/standard_message_codec/CHANGELOG.md +++ b/packages/standard_message_codec/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 0.0.1+3 * Minor README updates. diff --git a/packages/standard_message_codec/example/pubspec.yaml b/packages/standard_message_codec/example/pubspec.yaml index c8fa65531b4a..1978db746f1f 100644 --- a/packages/standard_message_codec/example/pubspec.yaml +++ b/packages/standard_message_codec/example/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=2.18.0 <4.0.0" dependencies: standard_message_codec: diff --git a/packages/standard_message_codec/pubspec.yaml b/packages/standard_message_codec/pubspec.yaml index da969f337cb4..fe73da0748fe 100644 --- a/packages/standard_message_codec/pubspec.yaml +++ b/packages/standard_message_codec/pubspec.yaml @@ -5,7 +5,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/standard_mess issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Astandard_message_codec environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=2.18.0 <4.0.0" dev_dependencies: test: ^1.16.0 diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index ea5f593f59e6..ebb56fde9496 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,5 +1,10 @@ ## NEXT +* Updates minimum supported macOS version to 10.14. + +## 6.1.11 + +* Fixes some url_launcher docs that were missing the null operator (?). * Updates minimum Flutter version to 3.3. * Aligns Dart and Flutter SDK constraints. diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md index b394e4ad6395..47196eb07a56 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -8,7 +8,7 @@ A Flutter plugin for launching a URL. | | Android | iOS | Linux | macOS | Web | Windows | |-------------|---------|-------|-------|--------|-----|-------------| -| **Support** | SDK 16+ | 11.0+ | Any | 10.11+ | Any | Windows 10+ | +| **Support** | SDK 16+ | 11.0+ | Any | 10.14+ | Any | Windows 10+ | ## Usage diff --git a/packages/url_launcher/url_launcher/example/.pluginToolsConfig.yaml b/packages/url_launcher/url_launcher/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/url_launcher/url_launcher/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/url_launcher/url_launcher/example/macos/Podfile b/packages/url_launcher/url_launcher/example/macos/Podfile index dade8dfad0dc..049abe295427 100644 --- a/packages/url_launcher/url_launcher/example/macos/Podfile +++ b/packages/url_launcher/url_launcher/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/url_launcher/url_launcher/example/macos/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher/example/macos/Runner.xcodeproj/project.pbxproj index a95e62daada1..7c8f758a2d63 100644 --- a/packages/url_launcher/url_launcher/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -26,10 +26,6 @@ 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 */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DD4A1B9DEDBB72C87CD7AE27 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5067D74CB28D28AE3B3DD05B /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -50,8 +46,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -70,7 +64,6 @@ 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 = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; 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 = ""; }; @@ -80,7 +73,6 @@ 899489AD6AA35AECA4E2BEA6 /* 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 = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; B36FDC1D769C9045B8821207 /* 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 = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,8 +80,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, DD4A1B9DEDBB72C87CD7AE27 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -145,8 +135,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; @@ -215,7 +203,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -268,6 +256,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -281,7 +270,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + 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; @@ -308,10 +297,13 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/url_launcher_macos/url_launcher_macos.framework", ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_macos.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -414,7 +406,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -497,7 +489,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -544,7 +536,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/url_launcher/url_launcher/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/url_launcher/url_launcher/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 660c47db95c3..caacd4ca7b49 100644 --- a/packages/url_launcher/url_launcher/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/url_launcher/url_launcher/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + + diff --git a/packages/url_launcher/url_launcher/lib/src/link.dart b/packages/url_launcher/url_launcher/lib/src/link.dart index d472321ee979..4e8f72d07cfe 100644 --- a/packages/url_launcher/url_launcher/lib/src/link.dart +++ b/packages/url_launcher/url_launcher/lib/src/link.dart @@ -25,7 +25,7 @@ Future Function(Object?, String) pushRouteToFrameworkFunction = /// ```dart /// Link( /// uri: Uri.parse('https://flutter.dev'), -/// builder: (BuildContext context, FollowLink followLink) => ElevatedButton( +/// builder: (BuildContext context, FollowLink? followLink) => ElevatedButton( /// onPressed: followLink, /// // ... other properties here ... /// )}, @@ -37,7 +37,7 @@ Future Function(Object?, String) pushRouteToFrameworkFunction = /// ```dart /// Link( /// uri: Uri.parse('/home'), -/// builder: (BuildContext context, FollowLink followLink) => ElevatedButton( +/// builder: (BuildContext context, FollowLink? followLink) => ElevatedButton( /// onPressed: followLink, /// // ... other properties here ... /// )}, diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 58dce23759b7..9bc9d598a51b 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.1.10 +version: 6.1.11 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/url_launcher/url_launcher_android/CHANGELOG.md b/packages/url_launcher/url_launcher_android/CHANGELOG.md index b37a9989cc7e..48abee6a10a1 100644 --- a/packages/url_launcher/url_launcher_android/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## 6.0.34 + +* Reverts ContextCompat usage that caused flutter/flutter#127014 + +## 6.0.33 + +* Explicitly sets if reciever for close should be exported. + ## 6.0.32 * Updates gradle, AGP and fixes some lint errors. diff --git a/packages/url_launcher/url_launcher_android/example/.pluginToolsConfig.yaml b/packages/url_launcher/url_launcher_android/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/url_launcher/url_launcher_android/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/url_launcher/url_launcher_android/pubspec.yaml b/packages/url_launcher/url_launcher_android/pubspec.yaml index 6f43020e3f80..6b1e168e9bc3 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.32 +version: 6.0.34 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 60e7b47447b9..5a2beeb0a2a7 100644 --- a/packages/url_launcher/url_launcher_linux/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 3.0.5 * Sets a cmake_policy compatibility version to fix build warnings. diff --git a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml index cb87799baeb2..6dbebd9463eb 100644 --- a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the url_launcher plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_linux/pubspec.yaml b/packages/url_launcher/url_launcher_linux/pubspec.yaml index cbae2775f212..1f639cc2d361 100644 --- a/packages/url_launcher/url_launcher_linux/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 3.0.5 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_macos/CHANGELOG.md b/packages/url_launcher/url_launcher_macos/CHANGELOG.md index f1534e91d744..bddec952ff75 100644 --- a/packages/url_launcher/url_launcher_macos/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_macos/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Updates minimum supported macOS version to 10.14. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 3.0.5 * Converts method channel to Pigeon. diff --git a/packages/url_launcher/url_launcher_macos/example/macos/Podfile b/packages/url_launcher/url_launcher_macos/example/macos/Podfile index e8da8332969a..47c1b18fedae 100644 --- a/packages/url_launcher/url_launcher_macos/example/macos/Podfile +++ b/packages/url_launcher/url_launcher_macos/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/project.pbxproj index 88c678b4a15d..abe80f431c64 100644 --- a/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -260,7 +260,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1250; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -325,6 +325,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -509,7 +510,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -592,7 +593,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -639,7 +640,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 323d07b817b1..b7ec90bbcd0d 100644 --- a/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_macos/macos/url_launcher_macos.podspec b/packages/url_launcher/url_launcher_macos/macos/url_launcher_macos.podspec index a3a940377ef4..70864ec4f36a 100644 --- a/packages/url_launcher/url_launcher_macos/macos/url_launcher_macos.podspec +++ b/packages/url_launcher/url_launcher_macos/macos/url_launcher_macos.podspec @@ -15,7 +15,7 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' - s.platform = :osx, '10.11' + s.platform = :osx, '10.14' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' end diff --git a/packages/url_launcher/url_launcher_macos/pubspec.yaml b/packages/url_launcher/url_launcher_macos/pubspec.yaml index 51b15e98fcbf..1c09ae7f43bf 100644 --- a/packages/url_launcher/url_launcher_macos/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 3.0.5 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index 91ef9e42e4fa..ad65f305d64d 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.0.17 +* Removes obsolete null checks on non-nullable values. * Updates minimum Flutter version to 3.3. ## 2.0.16 diff --git a/packages/url_launcher/url_launcher_web/lib/src/link.dart b/packages/url_launcher/url_launcher_web/lib/src/link.dart index df1fd140a2d1..ecb595c21f01 100644 --- a/packages/url_launcher/url_launcher_web/lib/src/link.dart +++ b/packages/url_launcher/url_launcher_web/lib/src/link.dart @@ -166,8 +166,6 @@ class LinkViewController extends PlatformViewController { late html.Element _element; - bool get _isInitialized => _element != null; - Future _initialize() async { _element = html.Element.tag('a'); setProperty(_element, linkViewIdProperty, viewId); @@ -219,7 +217,6 @@ class LinkViewController extends PlatformViewController { /// /// When Uri is null, the `href` attribute of the link is removed. void setUri(Uri? uri) { - assert(_isInitialized); _uri = uri; if (uri == null) { _element.removeAttribute('href'); @@ -236,7 +233,6 @@ class LinkViewController extends PlatformViewController { /// Set the [LinkTarget] value for this link. void setTarget(LinkTarget target) { - assert(_isInitialized); _element.setAttribute('target', _getHtmlTarget(target)); } @@ -270,14 +266,12 @@ class LinkViewController extends PlatformViewController { @override Future dispose() async { - if (_isInitialized) { - assert(_instances[viewId] == this); - _instances.remove(viewId); - if (_instances.isEmpty) { - await _clickSubscription.cancel(); - } - await SystemChannels.platform_views.invokeMethod('dispose', viewId); + assert(_instances[viewId] == this); + _instances.remove(viewId); + if (_instances.isEmpty) { + await _clickSubscription.cancel(); } + await SystemChannels.platform_views.invokeMethod('dispose', viewId); } } diff --git a/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/browser.dart b/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/browser.dart index 6935cb55df77..ed24b924d515 100644 --- a/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/browser.dart +++ b/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/browser.dart @@ -28,8 +28,5 @@ bool navigatorIsSafari(html.Navigator navigator) { // An web view running in an iOS app does not have a 'Version/X.X.X' string in the appVersion final String vendor = navigator.vendor; final String appVersion = navigator.appVersion; - return vendor != null && - vendor.contains('Apple') && - appVersion != null && - appVersion.contains('Version'); + return vendor.contains('Apple') && appVersion.contains('Version'); } diff --git a/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart b/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart index 636cd8c513a3..4764d5c48e06 100644 --- a/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart +++ b/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart @@ -83,8 +83,8 @@ class UrlLauncherPlugin extends UrlLauncherPlatform { bool universalLinksOnly = false, Map headers = const {}, String? webOnlyWindowName, - }) { - return Future.value( - openNewWindow(url, webOnlyWindowName: webOnlyWindowName) != null); + }) async { + openNewWindow(url, webOnlyWindowName: webOnlyWindowName); + return true; } } diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index 755dcd507a58..b8b1869681cd 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_web description: Web platform implementation of url_launcher repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.0.16 +version: 2.0.17 environment: sdk: ">=2.18.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 87da9c657a3f..5f840cb547cf 100644 --- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 3.0.6 * Sets a cmake_policy compatibility version to fix build warnings. diff --git a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml index da6b4718a0d4..93dc8ece0b6c 100644 --- a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates the Windows implementation of the url_launcher plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml index 217947684ce6..f91d6294c7ab 100644 --- a/packages/url_launcher/url_launcher_windows/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 3.0.6 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 368af5b1fd5e..7f24a73314a8 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.6.1 * Synchronizes `VideoPlayerValue.isPlaying` with underlying video player. diff --git a/packages/video_player/video_player/example/.pluginToolsConfig.yaml b/packages/video_player/video_player/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/video_player/video_player/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/video_player/video_player/example/pubspec.yaml b/packages/video_player/video_player/example/pubspec.yaml index 9fde956d6184..42bd6087522f 100644 --- a/packages/video_player/video_player/example/pubspec.yaml +++ b/packages/video_player/video_player/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the video_player plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 91d0a2ca8be5..f33a272afe32 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -6,8 +6,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.6.1 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index a204cb2c6e1c..b754df47418c 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.8 + +* Bumps ExoPlayer version to 2.18.6. + +## 2.4.7 + +* Fixes Java warnings. + ## 2.4.6 * Fixes compatibility with AGP versions older than 4.2. diff --git a/packages/video_player/video_player_android/android/build.gradle b/packages/video_player/video_player_android/android/build.gradle index 613b8ed3c298..5eb2185f1af3 100644 --- a/packages/video_player/video_player_android/android/build.gradle +++ b/packages/video_player/video_player_android/android/build.gradle @@ -41,7 +41,6 @@ android { checkAllWarnings true warningsAsErrors true disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' - baseline file("lint-baseline.xml") } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -49,7 +48,7 @@ android { } dependencies { - def exoplayer_version = "2.18.5" + def exoplayer_version = "2.18.6" implementation "com.google.android.exoplayer:exoplayer-core:${exoplayer_version}" implementation "com.google.android.exoplayer:exoplayer-hls:${exoplayer_version}" implementation "com.google.android.exoplayer:exoplayer-dash:${exoplayer_version}" diff --git a/packages/video_player/video_player_android/android/lint-baseline.xml b/packages/video_player/video_player_android/android/lint-baseline.xml deleted file mode 100644 index 4649a80f7b92..000000000000 --- a/packages/video_player/video_player_android/android/lint-baseline.xml +++ /dev/null @@ -1,653 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/CustomSSLSocketFactory.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/CustomSSLSocketFactory.java index fb6d2d4108cd..731bb9798f65 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/CustomSSLSocketFactory.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/CustomSSLSocketFactory.java @@ -13,8 +13,10 @@ import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; +// SSLSocketFactory does not have nullability annotations. +@SuppressWarnings("UnknownNullness") public class CustomSSLSocketFactory extends SSLSocketFactory { - private SSLSocketFactory sslSocketFactory; + private final SSLSocketFactory sslSocketFactory; public CustomSSLSocketFactory() throws KeyManagementException, NoSuchAlgorithmException { SSLContext context = SSLContext.getInstance("TLS"); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java index 6593ebf9c22a..e0790af8b86d 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.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 (v2.0.1), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.videoplayer; @@ -16,15 +16,47 @@ import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.HashMap; import java.util.Map; /** Generated class from Pigeon. */ -@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) public class Messages { + /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ + public static class FlutterError extends RuntimeException { + + /** The error code. */ + public final String code; + + /** The error details. Must be a datatype supported by the api codec. */ + public final Object details; + + public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) { + super(message); + this.code = code; + this.details = details; + } + } + + @NonNull + protected static ArrayList wrapError(@NonNull Throwable exception) { + ArrayList errorList = new ArrayList(3); + if (exception instanceof FlutterError) { + FlutterError error = (FlutterError) exception; + errorList.add(error.code); + errorList.add(error.getMessage()); + errorList.add(error.details); + } else { + errorList.add(exception.toString()); + errorList.add(exception.getClass().getSimpleName()); + errorList.add( + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + } + return errorList; + } + /** Generated class from Pigeon that represents data sent in messages. */ - public static class TextureMessage { + public static final class TextureMessage { private @NonNull Long textureId; public @NonNull Long getTextureId() { @@ -38,10 +70,11 @@ public void setTextureId(@NonNull Long setterArg) { this.textureId = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private TextureMessage() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + TextureMessage() {} + + public static final class Builder { - public static class Builder { private @Nullable Long textureId; public @NonNull Builder setTextureId(@NonNull Long setterArg) { @@ -57,15 +90,15 @@ public static class Builder { } @NonNull - Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("textureId", textureId); - return toMapResult; + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add(textureId); + return toListResult; } - static @NonNull TextureMessage fromMap(@NonNull Map map) { + static @NonNull TextureMessage fromList(@NonNull ArrayList list) { TextureMessage pigeonResult = new TextureMessage(); - Object textureId = map.get("textureId"); + Object textureId = list.get(0); pigeonResult.setTextureId( (textureId == null) ? null @@ -75,7 +108,7 @@ Map toMap() { } /** Generated class from Pigeon that represents data sent in messages. */ - public static class LoopingMessage { + public static final class LoopingMessage { private @NonNull Long textureId; public @NonNull Long getTextureId() { @@ -102,10 +135,11 @@ public void setIsLooping(@NonNull Boolean setterArg) { this.isLooping = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private LoopingMessage() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + LoopingMessage() {} + + public static final class Builder { - public static class Builder { private @Nullable Long textureId; public @NonNull Builder setTextureId(@NonNull Long setterArg) { @@ -129,28 +163,28 @@ public static class Builder { } @NonNull - Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("textureId", textureId); - toMapResult.put("isLooping", isLooping); - return toMapResult; + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(textureId); + toListResult.add(isLooping); + return toListResult; } - static @NonNull LoopingMessage fromMap(@NonNull Map map) { + static @NonNull LoopingMessage fromList(@NonNull ArrayList list) { LoopingMessage pigeonResult = new LoopingMessage(); - Object textureId = map.get("textureId"); + Object textureId = list.get(0); pigeonResult.setTextureId( (textureId == null) ? null : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId)); - Object isLooping = map.get("isLooping"); + Object isLooping = list.get(1); pigeonResult.setIsLooping((Boolean) isLooping); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class VolumeMessage { + public static final class VolumeMessage { private @NonNull Long textureId; public @NonNull Long getTextureId() { @@ -177,10 +211,11 @@ public void setVolume(@NonNull Double setterArg) { this.volume = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private VolumeMessage() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + VolumeMessage() {} + + public static final class Builder { - public static class Builder { private @Nullable Long textureId; public @NonNull Builder setTextureId(@NonNull Long setterArg) { @@ -204,28 +239,28 @@ public static class Builder { } @NonNull - Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("textureId", textureId); - toMapResult.put("volume", volume); - return toMapResult; + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(textureId); + toListResult.add(volume); + return toListResult; } - static @NonNull VolumeMessage fromMap(@NonNull Map map) { + static @NonNull VolumeMessage fromList(@NonNull ArrayList list) { VolumeMessage pigeonResult = new VolumeMessage(); - Object textureId = map.get("textureId"); + Object textureId = list.get(0); pigeonResult.setTextureId( (textureId == null) ? null : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId)); - Object volume = map.get("volume"); + Object volume = list.get(1); pigeonResult.setVolume((Double) volume); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class PlaybackSpeedMessage { + public static final class PlaybackSpeedMessage { private @NonNull Long textureId; public @NonNull Long getTextureId() { @@ -252,10 +287,11 @@ public void setSpeed(@NonNull Double setterArg) { this.speed = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private PlaybackSpeedMessage() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + PlaybackSpeedMessage() {} + + public static final class Builder { - public static class Builder { private @Nullable Long textureId; public @NonNull Builder setTextureId(@NonNull Long setterArg) { @@ -279,28 +315,28 @@ public static class Builder { } @NonNull - Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("textureId", textureId); - toMapResult.put("speed", speed); - return toMapResult; + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(textureId); + toListResult.add(speed); + return toListResult; } - static @NonNull PlaybackSpeedMessage fromMap(@NonNull Map map) { + static @NonNull PlaybackSpeedMessage fromList(@NonNull ArrayList list) { PlaybackSpeedMessage pigeonResult = new PlaybackSpeedMessage(); - Object textureId = map.get("textureId"); + Object textureId = list.get(0); pigeonResult.setTextureId( (textureId == null) ? null : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId)); - Object speed = map.get("speed"); + Object speed = list.get(1); pigeonResult.setSpeed((Double) speed); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class PositionMessage { + public static final class PositionMessage { private @NonNull Long textureId; public @NonNull Long getTextureId() { @@ -327,10 +363,11 @@ public void setPosition(@NonNull Long setterArg) { this.position = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private PositionMessage() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + PositionMessage() {} + + public static final class Builder { - public static class Builder { private @Nullable Long textureId; public @NonNull Builder setTextureId(@NonNull Long setterArg) { @@ -354,21 +391,21 @@ public static class Builder { } @NonNull - Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("textureId", textureId); - toMapResult.put("position", position); - return toMapResult; + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(textureId); + toListResult.add(position); + return toListResult; } - static @NonNull PositionMessage fromMap(@NonNull Map map) { + static @NonNull PositionMessage fromList(@NonNull ArrayList list) { PositionMessage pigeonResult = new PositionMessage(); - Object textureId = map.get("textureId"); + Object textureId = list.get(0); pigeonResult.setTextureId( (textureId == null) ? null : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId)); - Object position = map.get("position"); + Object position = list.get(1); pigeonResult.setPosition( (position == null) ? null @@ -378,7 +415,7 @@ Map toMap() { } /** Generated class from Pigeon that represents data sent in messages. */ - public static class CreateMessage { + public static final class CreateMessage { private @Nullable String asset; public @Nullable String getAsset() { @@ -432,10 +469,11 @@ public void setHttpHeaders(@NonNull Map setterArg) { this.httpHeaders = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private CreateMessage() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + CreateMessage() {} + + public static final class Builder { - public static class Builder { private @Nullable String asset; public @NonNull Builder setAsset(@Nullable String setterArg) { @@ -483,34 +521,34 @@ public static class Builder { } @NonNull - Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("asset", asset); - toMapResult.put("uri", uri); - toMapResult.put("packageName", packageName); - toMapResult.put("formatHint", formatHint); - toMapResult.put("httpHeaders", httpHeaders); - return toMapResult; + ArrayList toList() { + ArrayList toListResult = new ArrayList(5); + toListResult.add(asset); + toListResult.add(uri); + toListResult.add(packageName); + toListResult.add(formatHint); + toListResult.add(httpHeaders); + return toListResult; } - static @NonNull CreateMessage fromMap(@NonNull Map map) { + static @NonNull CreateMessage fromList(@NonNull ArrayList list) { CreateMessage pigeonResult = new CreateMessage(); - Object asset = map.get("asset"); + Object asset = list.get(0); pigeonResult.setAsset((String) asset); - Object uri = map.get("uri"); + Object uri = list.get(1); pigeonResult.setUri((String) uri); - Object packageName = map.get("packageName"); + Object packageName = list.get(2); pigeonResult.setPackageName((String) packageName); - Object formatHint = map.get("formatHint"); + Object formatHint = list.get(3); pigeonResult.setFormatHint((String) formatHint); - Object httpHeaders = map.get("httpHeaders"); + Object httpHeaders = list.get(4); pigeonResult.setHttpHeaders((Map) httpHeaders); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class MixWithOthersMessage { + public static final class MixWithOthersMessage { private @NonNull Boolean mixWithOthers; public @NonNull Boolean getMixWithOthers() { @@ -524,10 +562,11 @@ public void setMixWithOthers(@NonNull Boolean setterArg) { this.mixWithOthers = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private MixWithOthersMessage() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + MixWithOthersMessage() {} + + public static final class Builder { - public static class Builder { private @Nullable Boolean mixWithOthers; public @NonNull Builder setMixWithOthers(@NonNull Boolean setterArg) { @@ -543,15 +582,15 @@ public static class Builder { } @NonNull - Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("mixWithOthers", mixWithOthers); - return toMapResult; + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add(mixWithOthers); + return toListResult; } - static @NonNull MixWithOthersMessage fromMap(@NonNull Map map) { + static @NonNull MixWithOthersMessage fromList(@NonNull ArrayList list) { MixWithOthersMessage pigeonResult = new MixWithOthersMessage(); - Object mixWithOthers = map.get("mixWithOthers"); + Object mixWithOthers = list.get(0); pigeonResult.setMixWithOthers((Boolean) mixWithOthers); return pigeonResult; } @@ -563,57 +602,50 @@ private static class AndroidVideoPlayerApiCodec extends StandardMessageCodec { private AndroidVideoPlayerApiCodec() {} @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { case (byte) 128: - return CreateMessage.fromMap((Map) readValue(buffer)); - + return CreateMessage.fromList((ArrayList) readValue(buffer)); case (byte) 129: - return LoopingMessage.fromMap((Map) readValue(buffer)); - + return LoopingMessage.fromList((ArrayList) readValue(buffer)); case (byte) 130: - return MixWithOthersMessage.fromMap((Map) readValue(buffer)); - + return MixWithOthersMessage.fromList((ArrayList) readValue(buffer)); case (byte) 131: - return PlaybackSpeedMessage.fromMap((Map) readValue(buffer)); - + return PlaybackSpeedMessage.fromList((ArrayList) readValue(buffer)); case (byte) 132: - return PositionMessage.fromMap((Map) readValue(buffer)); - + return PositionMessage.fromList((ArrayList) readValue(buffer)); case (byte) 133: - return TextureMessage.fromMap((Map) readValue(buffer)); - + return TextureMessage.fromList((ArrayList) readValue(buffer)); case (byte) 134: - return VolumeMessage.fromMap((Map) readValue(buffer)); - + return VolumeMessage.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); } } @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof CreateMessage) { stream.write(128); - writeValue(stream, ((CreateMessage) value).toMap()); + writeValue(stream, ((CreateMessage) value).toList()); } else if (value instanceof LoopingMessage) { stream.write(129); - writeValue(stream, ((LoopingMessage) value).toMap()); + writeValue(stream, ((LoopingMessage) value).toList()); } else if (value instanceof MixWithOthersMessage) { stream.write(130); - writeValue(stream, ((MixWithOthersMessage) value).toMap()); + writeValue(stream, ((MixWithOthersMessage) value).toList()); } else if (value instanceof PlaybackSpeedMessage) { stream.write(131); - writeValue(stream, ((PlaybackSpeedMessage) value).toMap()); + writeValue(stream, ((PlaybackSpeedMessage) value).toList()); } else if (value instanceof PositionMessage) { stream.write(132); - writeValue(stream, ((PositionMessage) value).toMap()); + writeValue(stream, ((PositionMessage) value).toList()); } else if (value instanceof TextureMessage) { stream.write(133); - writeValue(stream, ((TextureMessage) value).toMap()); + writeValue(stream, ((TextureMessage) value).toList()); } else if (value instanceof VolumeMessage) { stream.write(134); - writeValue(stream, ((VolumeMessage) value).toMap()); + writeValue(stream, ((VolumeMessage) value).toList()); } else { super.writeValue(stream, value); } @@ -622,6 +654,7 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface AndroidVideoPlayerApi { + void initialize(); @NonNull @@ -647,15 +680,15 @@ public interface AndroidVideoPlayerApi { void setMixWithOthers(@NonNull MixWithOthersMessage msg); /** The codec used by AndroidVideoPlayerApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return AndroidVideoPlayerApiCodec.INSTANCE; } - /** * Sets up an instance of `AndroidVideoPlayerApi` to handle messages through the * `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, AndroidVideoPlayerApi api) { + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable AndroidVideoPlayerApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -663,12 +696,13 @@ static void setup(BinaryMessenger binaryMessenger, AndroidVideoPlayerApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList(); try { api.initialize(); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -683,17 +717,15 @@ static void setup(BinaryMessenger binaryMessenger, AndroidVideoPlayerApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + CreateMessage msgArg = (CreateMessage) args.get(0); try { - ArrayList args = (ArrayList) message; - CreateMessage msgArg = (CreateMessage) args.get(0); - if (msgArg == null) { - throw new NullPointerException("msgArg unexpectedly null."); - } TextureMessage output = api.create(msgArg); - wrapped.put("result", output); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -708,17 +740,15 @@ static void setup(BinaryMessenger binaryMessenger, AndroidVideoPlayerApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + TextureMessage msgArg = (TextureMessage) args.get(0); try { - ArrayList args = (ArrayList) message; - TextureMessage msgArg = (TextureMessage) args.get(0); - if (msgArg == null) { - throw new NullPointerException("msgArg unexpectedly null."); - } api.dispose(msgArg); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -733,17 +763,15 @@ static void setup(BinaryMessenger binaryMessenger, AndroidVideoPlayerApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + LoopingMessage msgArg = (LoopingMessage) args.get(0); try { - ArrayList args = (ArrayList) message; - LoopingMessage msgArg = (LoopingMessage) args.get(0); - if (msgArg == null) { - throw new NullPointerException("msgArg unexpectedly null."); - } api.setLooping(msgArg); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -758,17 +786,15 @@ static void setup(BinaryMessenger binaryMessenger, AndroidVideoPlayerApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + VolumeMessage msgArg = (VolumeMessage) args.get(0); try { - ArrayList args = (ArrayList) message; - VolumeMessage msgArg = (VolumeMessage) args.get(0); - if (msgArg == null) { - throw new NullPointerException("msgArg unexpectedly null."); - } api.setVolume(msgArg); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -785,17 +811,15 @@ static void setup(BinaryMessenger binaryMessenger, AndroidVideoPlayerApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + PlaybackSpeedMessage msgArg = (PlaybackSpeedMessage) args.get(0); try { - ArrayList args = (ArrayList) message; - PlaybackSpeedMessage msgArg = (PlaybackSpeedMessage) args.get(0); - if (msgArg == null) { - throw new NullPointerException("msgArg unexpectedly null."); - } api.setPlaybackSpeed(msgArg); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -810,17 +834,15 @@ static void setup(BinaryMessenger binaryMessenger, AndroidVideoPlayerApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + TextureMessage msgArg = (TextureMessage) args.get(0); try { - ArrayList args = (ArrayList) message; - TextureMessage msgArg = (TextureMessage) args.get(0); - if (msgArg == null) { - throw new NullPointerException("msgArg unexpectedly null."); - } api.play(msgArg); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -835,17 +857,15 @@ static void setup(BinaryMessenger binaryMessenger, AndroidVideoPlayerApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + TextureMessage msgArg = (TextureMessage) args.get(0); try { - ArrayList args = (ArrayList) message; - TextureMessage msgArg = (TextureMessage) args.get(0); - if (msgArg == null) { - throw new NullPointerException("msgArg unexpectedly null."); - } PositionMessage output = api.position(msgArg); - wrapped.put("result", output); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -860,17 +880,15 @@ static void setup(BinaryMessenger binaryMessenger, AndroidVideoPlayerApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + PositionMessage msgArg = (PositionMessage) args.get(0); try { - ArrayList args = (ArrayList) message; - PositionMessage msgArg = (PositionMessage) args.get(0); - if (msgArg == null) { - throw new NullPointerException("msgArg unexpectedly null."); - } api.seekTo(msgArg); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -885,17 +903,15 @@ static void setup(BinaryMessenger binaryMessenger, AndroidVideoPlayerApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + TextureMessage msgArg = (TextureMessage) args.get(0); try { - ArrayList args = (ArrayList) message; - TextureMessage msgArg = (TextureMessage) args.get(0); - if (msgArg == null) { - throw new NullPointerException("msgArg unexpectedly null."); - } api.pause(msgArg); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -912,17 +928,15 @@ static void setup(BinaryMessenger binaryMessenger, AndroidVideoPlayerApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + MixWithOthersMessage msgArg = (MixWithOthersMessage) args.get(0); try { - ArrayList args = (ArrayList) message; - MixWithOthersMessage msgArg = (MixWithOthersMessage) args.get(0); - if (msgArg == null) { - throw new NullPointerException("msgArg unexpectedly null."); - } api.setMixWithOthers(msgArg); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -932,14 +946,4 @@ static void setup(BinaryMessenger binaryMessenger, AndroidVideoPlayerApi api) { } } } - - private static Map wrapError(Throwable exception) { - Map errorMap = new HashMap<>(); - errorMap.put("message", exception.toString()); - errorMap.put("code", exception.getClass().getSimpleName()); - errorMap.put( - "details", - "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); - return errorMap; - } } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java index 981389583d2d..222a2241614d 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java @@ -18,7 +18,7 @@ */ final class QueuingEventSink implements EventChannel.EventSink { private EventChannel.EventSink delegate; - private ArrayList eventQueue = new ArrayList<>(); + private final ArrayList eventQueue = new ArrayList<>(); private boolean done = false; public void setDelegate(EventChannel.EventSink delegate) { @@ -69,7 +69,7 @@ private void maybeFlush() { eventQueue.clear(); } - private static class EndOfStreamEvent {} + static class EndOfStreamEvent {} private static class ErrorEvent { String code; diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 4701c7939fbf..57fc1037cc7b 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -83,7 +83,7 @@ final class VideoPlayer { DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context, httpDataSourceFactory); - MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context); + MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint); exoPlayer.setMediaSource(mediaSource); exoPlayer.prepare(); @@ -124,7 +124,7 @@ public void buildHttpDataSourceFactory(@NonNull Map httpHeaders) } private MediaSource buildMediaSource( - Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) { + Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint) { int type; if (formatHint == null) { type = Util.inferContentType(uri); @@ -225,7 +225,7 @@ public void onPlaybackStateChanged(final int playbackState) { } @Override - public void onPlayerError(final PlaybackException error) { + public void onPlayerError(@NonNull final PlaybackException error) { setBuffering(false); if (eventSink != null) { eventSink.error("VideoError", "Video player had error " + error, null); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index b2dfa944f6d9..c0c60279324b 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -7,6 +7,7 @@ import android.content.Context; import android.os.Build; import android.util.LongSparseArray; +import androidx.annotation.NonNull; import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.plugins.FlutterPlugin; @@ -32,7 +33,7 @@ public class VideoPlayerPlugin implements FlutterPlugin, AndroidVideoPlayerApi { private static final String TAG = "VideoPlayerPlugin"; private final LongSparseArray videoPlayers = new LongSparseArray<>(); private FlutterState flutterState; - private VideoPlayerOptions options = new VideoPlayerOptions(); + private final VideoPlayerOptions options = new VideoPlayerOptions(); /** Register this with the v2 embedding for the plugin to respond to lifecycle callbacks. */ public VideoPlayerPlugin() {} @@ -51,7 +52,8 @@ private VideoPlayerPlugin(io.flutter.plugin.common.PluginRegistry.Registrar regi /** Registers this with the stable v1 embedding. Will not respond to lifecycle events. */ @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) { final VideoPlayerPlugin plugin = new VideoPlayerPlugin(registrar); registrar.addViewDestroyListener( view -> { @@ -61,7 +63,7 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra } @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { try { HttpsURLConnection.setDefaultSSLSocketFactory(new CustomSSLSocketFactory()); @@ -87,7 +89,7 @@ public void onAttachedToEngine(FlutterPluginBinding binding) { } @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { if (flutterState == null) { Log.wtf(TAG, "Detached from the engine before registering to it."); } @@ -116,7 +118,7 @@ public void initialize() { disposeAllPlayers(); } - public TextureMessage create(CreateMessage arg) { + public @NonNull TextureMessage create(@NonNull CreateMessage arg) { TextureRegistry.SurfaceTextureEntry handle = flutterState.textureRegistry.createSurfaceTexture(); EventChannel eventChannel = @@ -139,10 +141,9 @@ public TextureMessage create(CreateMessage arg) { handle, "asset:///" + assetLookupKey, null, - new HashMap(), + new HashMap<>(), options); } else { - @SuppressWarnings("unchecked") Map httpHeaders = arg.getHttpHeaders(); player = new VideoPlayer( @@ -156,37 +157,36 @@ public TextureMessage create(CreateMessage arg) { } videoPlayers.put(handle.id(), player); - TextureMessage result = new TextureMessage.Builder().setTextureId(handle.id()).build(); - return result; + return new TextureMessage.Builder().setTextureId(handle.id()).build(); } - public void dispose(TextureMessage arg) { + public void dispose(@NonNull TextureMessage arg) { VideoPlayer player = videoPlayers.get(arg.getTextureId()); player.dispose(); videoPlayers.remove(arg.getTextureId()); } - public void setLooping(LoopingMessage arg) { + public void setLooping(@NonNull LoopingMessage arg) { VideoPlayer player = videoPlayers.get(arg.getTextureId()); player.setLooping(arg.getIsLooping()); } - public void setVolume(VolumeMessage arg) { + public void setVolume(@NonNull VolumeMessage arg) { VideoPlayer player = videoPlayers.get(arg.getTextureId()); player.setVolume(arg.getVolume()); } - public void setPlaybackSpeed(PlaybackSpeedMessage arg) { + public void setPlaybackSpeed(@NonNull PlaybackSpeedMessage arg) { VideoPlayer player = videoPlayers.get(arg.getTextureId()); player.setPlaybackSpeed(arg.getSpeed()); } - public void play(TextureMessage arg) { + public void play(@NonNull TextureMessage arg) { VideoPlayer player = videoPlayers.get(arg.getTextureId()); player.play(); } - public PositionMessage position(TextureMessage arg) { + public @NonNull PositionMessage position(@NonNull TextureMessage arg) { VideoPlayer player = videoPlayers.get(arg.getTextureId()); PositionMessage result = new PositionMessage.Builder() @@ -197,18 +197,18 @@ public PositionMessage position(TextureMessage arg) { return result; } - public void seekTo(PositionMessage arg) { + public void seekTo(@NonNull PositionMessage arg) { VideoPlayer player = videoPlayers.get(arg.getTextureId()); player.seekTo(arg.getPosition().intValue()); } - public void pause(TextureMessage arg) { + public void pause(@NonNull TextureMessage arg) { VideoPlayer player = videoPlayers.get(arg.getTextureId()); player.pause(); } @Override - public void setMixWithOthers(MixWithOthersMessage arg) { + public void setMixWithOthers(@NonNull MixWithOthersMessage arg) { options.mixWithOthers = arg.getMixWithOthers(); } @@ -221,11 +221,11 @@ private interface KeyForAssetAndPackageName { } private static final class FlutterState { - private final Context applicationContext; - private final BinaryMessenger binaryMessenger; - private final KeyForAssetFn keyForAsset; - private final KeyForAssetAndPackageName keyForAssetAndPackageName; - private final TextureRegistry textureRegistry; + final Context applicationContext; + final BinaryMessenger binaryMessenger; + final KeyForAssetFn keyForAsset; + final KeyForAssetAndPackageName keyForAssetAndPackageName; + final TextureRegistry textureRegistry; FlutterState( Context applicationContext, diff --git a/packages/video_player/video_player_android/example/.pluginToolsConfig.yaml b/packages/video_player/video_player_android/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/video_player/video_player_android/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/video_player/video_player_android/lib/src/messages.g.dart b/packages/video_player/video_player_android/lib/src/messages.g.dart index 0dadd2efc67e..47ed6a295c83 100644 --- a/packages/video_player/video_player_android/lib/src/messages.g.dart +++ b/packages/video_player/video_player_android/lib/src/messages.g.dart @@ -1,14 +1,14 @@ // 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 (v2.0.1), 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 -// @dart = 2.12 +// 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 Uint8List, Int32List, Int64List, Float64List; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; class TextureMessage { @@ -19,15 +19,15 @@ class TextureMessage { int textureId; Object encode() { - final Map pigeonMap = {}; - pigeonMap['textureId'] = textureId; - return pigeonMap; + return [ + textureId, + ]; } - static TextureMessage decode(Object message) { - final Map pigeonMap = message as Map; + static TextureMessage decode(Object result) { + result as List; return TextureMessage( - textureId: pigeonMap['textureId']! as int, + textureId: result[0]! as int, ); } } @@ -39,20 +39,21 @@ class LoopingMessage { }); int textureId; + bool isLooping; Object encode() { - final Map pigeonMap = {}; - pigeonMap['textureId'] = textureId; - pigeonMap['isLooping'] = isLooping; - return pigeonMap; + return [ + textureId, + isLooping, + ]; } - static LoopingMessage decode(Object message) { - final Map pigeonMap = message as Map; + static LoopingMessage decode(Object result) { + result as List; return LoopingMessage( - textureId: pigeonMap['textureId']! as int, - isLooping: pigeonMap['isLooping']! as bool, + textureId: result[0]! as int, + isLooping: result[1]! as bool, ); } } @@ -64,20 +65,21 @@ class VolumeMessage { }); int textureId; + double volume; Object encode() { - final Map pigeonMap = {}; - pigeonMap['textureId'] = textureId; - pigeonMap['volume'] = volume; - return pigeonMap; + return [ + textureId, + volume, + ]; } - static VolumeMessage decode(Object message) { - final Map pigeonMap = message as Map; + static VolumeMessage decode(Object result) { + result as List; return VolumeMessage( - textureId: pigeonMap['textureId']! as int, - volume: pigeonMap['volume']! as double, + textureId: result[0]! as int, + volume: result[1]! as double, ); } } @@ -89,20 +91,21 @@ class PlaybackSpeedMessage { }); int textureId; + double speed; Object encode() { - final Map pigeonMap = {}; - pigeonMap['textureId'] = textureId; - pigeonMap['speed'] = speed; - return pigeonMap; + return [ + textureId, + speed, + ]; } - static PlaybackSpeedMessage decode(Object message) { - final Map pigeonMap = message as Map; + static PlaybackSpeedMessage decode(Object result) { + result as List; return PlaybackSpeedMessage( - textureId: pigeonMap['textureId']! as int, - speed: pigeonMap['speed']! as double, + textureId: result[0]! as int, + speed: result[1]! as double, ); } } @@ -114,20 +117,21 @@ class PositionMessage { }); int textureId; + int position; Object encode() { - final Map pigeonMap = {}; - pigeonMap['textureId'] = textureId; - pigeonMap['position'] = position; - return pigeonMap; + return [ + textureId, + position, + ]; } - static PositionMessage decode(Object message) { - final Map pigeonMap = message as Map; + static PositionMessage decode(Object result) { + result as List; return PositionMessage( - textureId: pigeonMap['textureId']! as int, - position: pigeonMap['position']! as int, + textureId: result[0]! as int, + position: result[1]! as int, ); } } @@ -142,30 +146,34 @@ class CreateMessage { }); String? asset; + String? uri; + String? packageName; + String? formatHint; + Map httpHeaders; Object encode() { - final Map pigeonMap = {}; - pigeonMap['asset'] = asset; - pigeonMap['uri'] = uri; - pigeonMap['packageName'] = packageName; - pigeonMap['formatHint'] = formatHint; - pigeonMap['httpHeaders'] = httpHeaders; - return pigeonMap; + return [ + asset, + uri, + packageName, + formatHint, + httpHeaders, + ]; } - static CreateMessage decode(Object message) { - final Map pigeonMap = message as Map; + static CreateMessage decode(Object result) { + result as List; return CreateMessage( - asset: pigeonMap['asset'] as String?, - uri: pigeonMap['uri'] as String?, - packageName: pigeonMap['packageName'] as String?, - formatHint: pigeonMap['formatHint'] as String?, - httpHeaders: (pigeonMap['httpHeaders'] as Map?)! - .cast(), + asset: result[0] as String?, + uri: result[1] as String?, + packageName: result[2] as String?, + formatHint: result[3] as String?, + httpHeaders: + (result[4] as Map?)!.cast(), ); } } @@ -178,15 +186,15 @@ class MixWithOthersMessage { bool mixWithOthers; Object encode() { - final Map pigeonMap = {}; - pigeonMap['mixWithOthers'] = mixWithOthers; - return pigeonMap; + return [ + mixWithOthers, + ]; } - static MixWithOthersMessage decode(Object message) { - final Map pigeonMap = message as Map; + static MixWithOthersMessage decode(Object result) { + result as List; return MixWithOthersMessage( - mixWithOthers: pigeonMap['mixWithOthers']! as bool, + mixWithOthers: result[0]! as bool, ); } } @@ -226,25 +234,18 @@ class _AndroidVideoPlayerApiCodec extends StandardMessageCodec { switch (type) { case 128: return CreateMessage.decode(readValue(buffer)!); - case 129: return LoopingMessage.decode(readValue(buffer)!); - case 130: return MixWithOthersMessage.decode(readValue(buffer)!); - case 131: return PlaybackSpeedMessage.decode(readValue(buffer)!); - case 132: return PositionMessage.decode(readValue(buffer)!); - case 133: return TextureMessage.decode(readValue(buffer)!); - case 134: return VolumeMessage.decode(readValue(buffer)!); - default: return super.readValueOfType(type, buffer); } @@ -257,7 +258,6 @@ class AndroidVideoPlayerApi { /// BinaryMessenger will be used which routes to the host platform. AndroidVideoPlayerApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _AndroidVideoPlayerApiCodec(); @@ -266,20 +266,17 @@ class AndroidVideoPlayerApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AndroidVideoPlayerApi.initialize', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 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 (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -290,28 +287,26 @@ class AndroidVideoPlayerApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AndroidVideoPlayerApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_msg]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as TextureMessage?)!; + return (replyList[0] as TextureMessage?)!; } } @@ -319,20 +314,18 @@ class AndroidVideoPlayerApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AndroidVideoPlayerApi.dispose', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_msg]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -343,20 +336,18 @@ class AndroidVideoPlayerApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AndroidVideoPlayerApi.setLooping', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_msg]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -367,20 +358,18 @@ class AndroidVideoPlayerApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AndroidVideoPlayerApi.setVolume', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_msg]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -391,20 +380,18 @@ class AndroidVideoPlayerApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AndroidVideoPlayerApi.setPlaybackSpeed', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_msg]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -415,20 +402,18 @@ class AndroidVideoPlayerApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AndroidVideoPlayerApi.play', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_msg]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -439,28 +424,26 @@ class AndroidVideoPlayerApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AndroidVideoPlayerApi.position', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_msg]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as PositionMessage?)!; + return (replyList[0] as PositionMessage?)!; } } @@ -468,20 +451,18 @@ class AndroidVideoPlayerApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AndroidVideoPlayerApi.seekTo', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_msg]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -492,20 +473,18 @@ class AndroidVideoPlayerApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AndroidVideoPlayerApi.pause', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_msg]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -516,20 +495,18 @@ class AndroidVideoPlayerApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AndroidVideoPlayerApi.setMixWithOthers', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_msg]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index dc195b440761..35026b9b77ba 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.6 +version: 2.4.8 environment: sdk: ">=2.18.0 <4.0.0" @@ -25,4 +25,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pigeon: ^2.0.1 + pigeon: ^9.2.5 diff --git a/packages/video_player/video_player_android/test/test_api.g.dart b/packages/video_player/video_player_android/test/test_api.g.dart index 6361522e247c..bb68e71c2fb8 100644 --- a/packages/video_player/video_player_android/test/test_api.g.dart +++ b/packages/video_player/video_player_android/test/test_api.g.dart @@ -1,20 +1,16 @@ // 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 (v2.0.1), 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 +// 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 -// @dart = 2.12 import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; -// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) -// ignore: unnecessary_import -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -// TODO(gaaclarke): This had to be hand tweaked from a relative path. import 'package:video_player_android/src/messages.g.dart'; class _TestHostVideoPlayerApiCodec extends StandardMessageCodec { @@ -52,25 +48,18 @@ class _TestHostVideoPlayerApiCodec extends StandardMessageCodec { switch (type) { case 128: return CreateMessage.decode(readValue(buffer)!); - case 129: return LoopingMessage.decode(readValue(buffer)!); - case 130: return MixWithOthersMessage.decode(readValue(buffer)!); - case 131: return PlaybackSpeedMessage.decode(readValue(buffer)!); - case 132: return PositionMessage.decode(readValue(buffer)!); - case 133: return TextureMessage.decode(readValue(buffer)!); - case 134: return VolumeMessage.decode(readValue(buffer)!); - default: return super.readValueOfType(type, buffer); } @@ -78,19 +67,32 @@ class _TestHostVideoPlayerApiCodec extends StandardMessageCodec { } abstract class TestHostVideoPlayerApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = _TestHostVideoPlayerApiCodec(); void initialize(); + TextureMessage create(CreateMessage msg); + void dispose(TextureMessage msg); + void setLooping(LoopingMessage msg); + void setVolume(VolumeMessage msg); + void setPlaybackSpeed(PlaybackSpeedMessage msg); + void play(TextureMessage msg); + PositionMessage position(TextureMessage msg); + void seekTo(PositionMessage msg); + void pause(TextureMessage msg); + void setMixWithOthers(MixWithOthersMessage msg); + static void setup(TestHostVideoPlayerApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -98,12 +100,15 @@ abstract class TestHostVideoPlayerApi { 'dev.flutter.pigeon.AndroidVideoPlayerApi.initialize', 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.initialize(); - return {}; + return []; }); } } @@ -112,9 +117,12 @@ abstract class TestHostVideoPlayerApi { 'dev.flutter.pigeon.AndroidVideoPlayerApi.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.AndroidVideoPlayerApi.create was null.'); final List args = (message as List?)!; @@ -122,7 +130,7 @@ abstract class TestHostVideoPlayerApi { assert(arg_msg != null, 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.create was null, expected non-null CreateMessage.'); final TextureMessage output = api.create(arg_msg!); - return {'result': output}; + return [output]; }); } } @@ -131,9 +139,12 @@ abstract class TestHostVideoPlayerApi { 'dev.flutter.pigeon.AndroidVideoPlayerApi.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.AndroidVideoPlayerApi.dispose was null.'); final List args = (message as List?)!; @@ -141,7 +152,7 @@ abstract class TestHostVideoPlayerApi { assert(arg_msg != null, 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.dispose was null, expected non-null TextureMessage.'); api.dispose(arg_msg!); - return {}; + return []; }); } } @@ -150,9 +161,12 @@ abstract class TestHostVideoPlayerApi { 'dev.flutter.pigeon.AndroidVideoPlayerApi.setLooping', 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.AndroidVideoPlayerApi.setLooping was null.'); final List args = (message as List?)!; @@ -160,7 +174,7 @@ abstract class TestHostVideoPlayerApi { assert(arg_msg != null, 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.setLooping was null, expected non-null LoopingMessage.'); api.setLooping(arg_msg!); - return {}; + return []; }); } } @@ -169,9 +183,12 @@ abstract class TestHostVideoPlayerApi { 'dev.flutter.pigeon.AndroidVideoPlayerApi.setVolume', 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.AndroidVideoPlayerApi.setVolume was null.'); final List args = (message as List?)!; @@ -179,7 +196,7 @@ abstract class TestHostVideoPlayerApi { assert(arg_msg != null, 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.setVolume was null, expected non-null VolumeMessage.'); api.setVolume(arg_msg!); - return {}; + return []; }); } } @@ -188,9 +205,12 @@ abstract class TestHostVideoPlayerApi { 'dev.flutter.pigeon.AndroidVideoPlayerApi.setPlaybackSpeed', 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.AndroidVideoPlayerApi.setPlaybackSpeed was null.'); final List args = (message as List?)!; @@ -199,7 +219,7 @@ abstract class TestHostVideoPlayerApi { assert(arg_msg != null, 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.setPlaybackSpeed was null, expected non-null PlaybackSpeedMessage.'); api.setPlaybackSpeed(arg_msg!); - return {}; + return []; }); } } @@ -208,9 +228,12 @@ abstract class TestHostVideoPlayerApi { 'dev.flutter.pigeon.AndroidVideoPlayerApi.play', 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.AndroidVideoPlayerApi.play was null.'); final List args = (message as List?)!; @@ -218,7 +241,7 @@ abstract class TestHostVideoPlayerApi { assert(arg_msg != null, 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.play was null, expected non-null TextureMessage.'); api.play(arg_msg!); - return {}; + return []; }); } } @@ -227,9 +250,12 @@ abstract class TestHostVideoPlayerApi { 'dev.flutter.pigeon.AndroidVideoPlayerApi.position', 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.AndroidVideoPlayerApi.position was null.'); final List args = (message as List?)!; @@ -237,7 +263,7 @@ abstract class TestHostVideoPlayerApi { assert(arg_msg != null, 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.position was null, expected non-null TextureMessage.'); final PositionMessage output = api.position(arg_msg!); - return {'result': output}; + return [output]; }); } } @@ -246,9 +272,12 @@ abstract class TestHostVideoPlayerApi { 'dev.flutter.pigeon.AndroidVideoPlayerApi.seekTo', 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.AndroidVideoPlayerApi.seekTo was null.'); final List args = (message as List?)!; @@ -256,7 +285,7 @@ abstract class TestHostVideoPlayerApi { assert(arg_msg != null, 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.seekTo was null, expected non-null PositionMessage.'); api.seekTo(arg_msg!); - return {}; + return []; }); } } @@ -265,9 +294,12 @@ abstract class TestHostVideoPlayerApi { 'dev.flutter.pigeon.AndroidVideoPlayerApi.pause', 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.AndroidVideoPlayerApi.pause was null.'); final List args = (message as List?)!; @@ -275,7 +307,7 @@ abstract class TestHostVideoPlayerApi { assert(arg_msg != null, 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.pause was null, expected non-null TextureMessage.'); api.pause(arg_msg!); - return {}; + return []; }); } } @@ -284,9 +316,12 @@ abstract class TestHostVideoPlayerApi { 'dev.flutter.pigeon.AndroidVideoPlayerApi.setMixWithOthers', 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.AndroidVideoPlayerApi.setMixWithOthers was null.'); final List args = (message as List?)!; @@ -295,7 +330,7 @@ abstract class TestHostVideoPlayerApi { assert(arg_msg != null, 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.setMixWithOthers was null, expected non-null MixWithOthersMessage.'); api.setMixWithOthers(arg_msg!); - return {}; + return []; }); } } diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 8c1e2bc3426f..a54aef55877f 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,15 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 2.4.6 + +* Fixes hang when seeking to end of video. + +## 2.4.5 + +* Updates functions without a prototype to avoid deprecation warning. + ## 2.4.4 * Updates pigeon to fix warnings with clang 15. diff --git a/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart b/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart index ae3cd7e3ea89..d8a73b09d24a 100644 --- a/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart @@ -71,13 +71,27 @@ void main() { expect(await controller.position, greaterThan(Duration.zero)); }); - testWidgets('can seek', (WidgetTester tester) async { - await controller.initialize(); + testWidgets( + 'can seek', + (WidgetTester tester) async { + await controller.initialize(); - await controller.seekTo(const Duration(seconds: 3)); + await controller.seekTo(const Duration(seconds: 3)); - expect(await controller.position, const Duration(seconds: 3)); - }); + expect(controller.value.position, const Duration(seconds: 3)); + }, + ); + + testWidgets( + 'can seek to end', + (WidgetTester tester) async { + await controller.initialize(); + + await controller.seekTo(controller.value.duration); + + expect(controller.value.duration, controller.value.position); + }, + ); testWidgets('can be paused', (WidgetTester tester) async { await controller.initialize(); diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m index 1ec18e762ae4..a9d7eac073df 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m @@ -8,6 +8,7 @@ #import #import +#import @interface FLTVideoPlayer : NSObject @property(readonly, nonatomic) AVPlayer *player; @@ -61,6 +62,46 @@ - (CGAffineTransform)preferredTransform { @interface VideoPlayerTests : XCTestCase @end +@interface StubAVPlayer : AVPlayer +@property(readonly, nonatomic) NSNumber *beforeTolerance; +@property(readonly, nonatomic) NSNumber *afterTolerance; +@end + +@implementation StubAVPlayer + +- (void)seekToTime:(CMTime)time + toleranceBefore:(CMTime)toleranceBefore + toleranceAfter:(CMTime)toleranceAfter + completionHandler:(void (^)(BOOL finished))completionHandler { + _beforeTolerance = [NSNumber numberWithLong:toleranceBefore.value]; + _afterTolerance = [NSNumber numberWithLong:toleranceAfter.value]; + completionHandler(YES); +} + +@end + +@interface StubFVPPlayerFactory : NSObject + +@property(nonatomic, strong) StubAVPlayer *stubAVPlayer; + +- (instancetype)initWithPlayer:(StubAVPlayer *)stubAVPlayer; + +@end + +@implementation StubFVPPlayerFactory + +- (instancetype)initWithPlayer:(StubAVPlayer *)stubAVPlayer { + self = [super init]; + _stubAVPlayer = stubAVPlayer; + return self; +} + +- (AVPlayer *)playerWithPlayerItem:(AVPlayerItem *)playerItem { + return _stubAVPlayer; +} + +@end + @implementation VideoPlayerTests - (void)testBlankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream { @@ -226,6 +267,81 @@ - (void)testTransformFix { [self validateTransformFixForOrientation:UIImageOrientationRightMirrored]; } +- (void)testSeekToleranceWhenNotSeekingToEnd { + NSObject *registry = + (NSObject *)[[UIApplication sharedApplication] delegate]; + NSObject *registrar = [registry registrarForPlugin:@"TestSeekTolerance"]; + + StubAVPlayer *stubAVPlayer = [[StubAVPlayer alloc] init]; + StubFVPPlayerFactory *stubFVPPlayerFactory = + [[StubFVPPlayerFactory alloc] initWithPlayer:stubAVPlayer]; + FLTVideoPlayerPlugin *pluginWithMockAVPlayer = + [[FLTVideoPlayerPlugin alloc] initWithPlayerFactory:stubFVPPlayerFactory registrar:registrar]; + + FlutterError *error; + [pluginWithMockAVPlayer initialize:&error]; + XCTAssertNil(error); + + FLTCreateMessage *create = [FLTCreateMessage + makeWithAsset:nil + uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" + packageName:nil + formatHint:nil + httpHeaders:@{}]; + FLTTextureMessage *textureMessage = [pluginWithMockAVPlayer create:create error:&error]; + NSNumber *textureId = textureMessage.textureId; + + XCTestExpectation *initializedExpectation = + [self expectationWithDescription:@"seekTo has zero tolerance when seeking not to end"]; + FLTPositionMessage *message = [FLTPositionMessage makeWithTextureId:textureId position:@1234]; + [pluginWithMockAVPlayer seekTo:message + completion:^(FlutterError *_Nullable error) { + [initializedExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + XCTAssertEqual([stubAVPlayer.beforeTolerance intValue], 0); + XCTAssertEqual([stubAVPlayer.afterTolerance intValue], 0); +} + +- (void)testSeekToleranceWhenSeekingToEnd { + NSObject *registry = + (NSObject *)[[UIApplication sharedApplication] delegate]; + NSObject *registrar = + [registry registrarForPlugin:@"TestSeekToEndTolerance"]; + + StubAVPlayer *stubAVPlayer = [[StubAVPlayer alloc] init]; + StubFVPPlayerFactory *stubFVPPlayerFactory = + [[StubFVPPlayerFactory alloc] initWithPlayer:stubAVPlayer]; + FLTVideoPlayerPlugin *pluginWithMockAVPlayer = + [[FLTVideoPlayerPlugin alloc] initWithPlayerFactory:stubFVPPlayerFactory registrar:registrar]; + + FlutterError *error; + [pluginWithMockAVPlayer initialize:&error]; + XCTAssertNil(error); + + FLTCreateMessage *create = [FLTCreateMessage + makeWithAsset:nil + uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" + packageName:nil + formatHint:nil + httpHeaders:@{}]; + FLTTextureMessage *textureMessage = [pluginWithMockAVPlayer create:create error:&error]; + NSNumber *textureId = textureMessage.textureId; + + XCTestExpectation *initializedExpectation = + [self expectationWithDescription:@"seekTo has non-zero tolerance when seeking to end"]; + // The duration of this video is "0" due to the non standard initiliatazion process. + FLTPositionMessage *message = [FLTPositionMessage makeWithTextureId:textureId position:@0]; + [pluginWithMockAVPlayer seekTo:message + completion:^(FlutterError *_Nullable error) { + [initializedExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + XCTAssertGreaterThan([stubAVPlayer.beforeTolerance intValue], 0); + XCTAssertGreaterThan([stubAVPlayer.afterTolerance intValue], 0); +} + - (NSDictionary *)testPlugin:(FLTVideoPlayerPlugin *)videoPlayerPlugin uri:(NSString *)uri { FlutterError *error; diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml index bffa17d11e17..e77e39a3c6a2 100644 --- a/packages/video_player/video_player_avfoundation/example/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the video_player plugin. publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m index 586d6555befb..ccece1127699 100644 --- a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m @@ -3,6 +3,7 @@ // found in the LICENSE file. #import "FLTVideoPlayerPlugin.h" +#import "FLTVideoPlayerPlugin_Test.h" #import #import @@ -33,6 +34,16 @@ - (void)onDisplayLink:(CADisplayLink *)link { } @end +@interface FVPDefaultPlayerFactory : NSObject +@end + +@implementation FVPDefaultPlayerFactory +- (AVPlayer *)playerWithPlayerItem:(AVPlayerItem *)playerItem { + return [AVPlayer playerWithPlayerItem:playerItem]; +} + +@end + @interface FLTVideoPlayer : NSObject @property(readonly, nonatomic) AVPlayer *player; @property(readonly, nonatomic) AVPlayerItemVideoOutput *videoOutput; @@ -52,7 +63,8 @@ @interface FLTVideoPlayer : NSObject @property(nonatomic, readonly) BOOL isInitialized; - (instancetype)initWithURL:(NSURL *)url frameUpdater:(FLTFrameUpdater *)frameUpdater - httpHeaders:(nonnull NSDictionary *)headers; + httpHeaders:(nonnull NSDictionary *)headers + playerFactory:(id)playerFactory; @end static void *timeRangeContext = &timeRangeContext; @@ -65,9 +77,14 @@ - (instancetype)initWithURL:(NSURL *)url static void *rateContext = &rateContext; @implementation FLTVideoPlayer -- (instancetype)initWithAsset:(NSString *)asset frameUpdater:(FLTFrameUpdater *)frameUpdater { +- (instancetype)initWithAsset:(NSString *)asset + frameUpdater:(FLTFrameUpdater *)frameUpdater + playerFactory:(id)playerFactory { NSString *path = [[NSBundle mainBundle] pathForResource:asset ofType:nil]; - return [self initWithURL:[NSURL fileURLWithPath:path] frameUpdater:frameUpdater httpHeaders:@{}]; + return [self initWithURL:[NSURL fileURLWithPath:path] + frameUpdater:frameUpdater + httpHeaders:@{} + playerFactory:playerFactory]; } - (void)addObserversForItem:(AVPlayerItem *)item player:(AVPlayer *)player { @@ -146,7 +163,7 @@ NS_INLINE CGFloat radiansToDegrees(CGFloat radians) { return degrees; }; -NS_INLINE UIViewController *rootViewController() { +NS_INLINE UIViewController *rootViewController(void) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" // TODO: (hellohuanlin) Provide a non-deprecated codepath. See @@ -203,18 +220,20 @@ - (void)createVideoOutputAndDisplayLink:(FLTFrameUpdater *)frameUpdater { - (instancetype)initWithURL:(NSURL *)url frameUpdater:(FLTFrameUpdater *)frameUpdater - httpHeaders:(nonnull NSDictionary *)headers { + httpHeaders:(nonnull NSDictionary *)headers + playerFactory:(id)playerFactory { NSDictionary *options = nil; if ([headers count] != 0) { options = @{@"AVURLAssetHTTPHeaderFieldsKey" : headers}; } AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:options]; AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset]; - return [self initWithPlayerItem:item frameUpdater:frameUpdater]; + return [self initWithPlayerItem:item frameUpdater:frameUpdater playerFactory:playerFactory]; } - (instancetype)initWithPlayerItem:(AVPlayerItem *)item - frameUpdater:(FLTFrameUpdater *)frameUpdater { + frameUpdater:(FLTFrameUpdater *)frameUpdater + playerFactory:(id)playerFactory { self = [super init]; NSAssert(self, @"super init cannot be nil"); @@ -247,7 +266,7 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item } }; - _player = [AVPlayer playerWithPlayerItem:item]; + _player = [playerFactory playerWithPlayerItem:item]; _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; // This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 @@ -420,9 +439,15 @@ - (int64_t)duration { } - (void)seekTo:(int)location completionHandler:(void (^)(BOOL))completionHandler { - [_player seekToTime:CMTimeMake(location, 1000) - toleranceBefore:kCMTimeZero - toleranceAfter:kCMTimeZero + CMTime locationCMT = CMTimeMake(location, 1000); + CMTimeValue duration = _player.currentItem.asset.duration.value; + // Without adding tolerance when seeking to duration, + // seekToTime will never complete, and this call will hang. + // see issue https://github.com/flutter/flutter/issues/124475. + CMTime tolerance = location == duration ? CMTimeMake(1, 1000) : kCMTimeZero; + [_player seekToTime:locationCMT + toleranceBefore:tolerance + toleranceAfter:tolerance completionHandler:completionHandler]; } @@ -523,6 +548,7 @@ @interface FLTVideoPlayerPlugin () @property(readonly, strong, nonatomic) NSMutableDictionary *playersByTextureId; @property(readonly, strong, nonatomic) NSObject *registrar; +@property(nonatomic, strong) id playerFactory; @end @implementation FLTVideoPlayerPlugin @@ -533,11 +559,17 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } - (instancetype)initWithRegistrar:(NSObject *)registrar { + return [self initWithPlayerFactory:[[FVPDefaultPlayerFactory alloc] init] registrar:registrar]; +} + +- (instancetype)initWithPlayerFactory:(id)playerFactory + registrar:(NSObject *)registrar { self = [super init]; NSAssert(self, @"super init cannot be nil"); _registry = [registrar textures]; _messenger = [registrar messenger]; _registrar = registrar; + _playerFactory = playerFactory; _playersByTextureId = [NSMutableDictionary dictionaryWithCapacity:1]; return self; } @@ -588,12 +620,15 @@ - (FLTTextureMessage *)create:(FLTCreateMessage *)input error:(FlutterError **)e } else { assetPath = [_registrar lookupKeyForAsset:input.asset]; } - player = [[FLTVideoPlayer alloc] initWithAsset:assetPath frameUpdater:frameUpdater]; + player = [[FLTVideoPlayer alloc] initWithAsset:assetPath + frameUpdater:frameUpdater + playerFactory:_playerFactory]; return [self onPlayerSetup:player frameUpdater:frameUpdater]; } else if (input.uri) { player = [[FLTVideoPlayer alloc] initWithURL:[NSURL URLWithString:input.uri] frameUpdater:frameUpdater - httpHeaders:input.httpHeaders]; + httpHeaders:input.httpHeaders + playerFactory:_playerFactory]; return [self onPlayerSetup:player frameUpdater:frameUpdater]; } else { *error = [FlutterError errorWithCode:@"video_player" message:@"not implemented" details:nil]; diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin_Test.h b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin_Test.h new file mode 100644 index 000000000000..4c52ba00f963 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin_Test.h @@ -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 "FLTVideoPlayerPlugin.h" + +#import + +// Protocol for an AVPlayer instance factory. Used for injecting players in tests. +@protocol FVPPlayerFactory +- (AVPlayer *)playerWithPlayerItem:(AVPlayerItem *)playerItem; +@end + +@interface FLTVideoPlayerPlugin () + +- (instancetype)initWithPlayerFactory:(id)playerFactory + registrar:(NSObject *)registrar; +@end diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index c579ee0be2e4..093f5ebbb51b 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_avfoundation description: iOS implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation 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/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 55b3f5cffde5..f75c5a10f94e 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 6.1.0 * Aligns Dart and Flutter SDK constraints. diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index 82bbdf782515..f266f6eaded3 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -7,8 +7,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 6.1.0 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 19f5bb927f2a..1bb8b187f89b 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.0.16 * Synchronizes `VideoPlayerValue.isPlaying` with `VideoElement`. diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml index 4cb1d4d7fa23..eb29488a243c 100644 --- a/packages/video_player/video_player_web/example/pubspec.yaml +++ b/packages/video_player/video_player_web/example/pubspec.yaml @@ -2,8 +2,8 @@ name: video_player_for_web_integration_tests publish_to: none environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index 70434ef882d0..d1a5eb61e8bd 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.0.16 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/web_benchmarks/CHANGELOG.md b/packages/web_benchmarks/CHANGELOG.md index dff0d5a66fc4..f01d493d7922 100644 --- a/packages/web_benchmarks/CHANGELOG.md +++ b/packages/web_benchmarks/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.1.0+4 + +* Removes obsolete null checks on non-nullable values. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 0.1.0+3 * Migrates from SingletonFlutterWindow to PlatformDispatcher API. diff --git a/packages/web_benchmarks/lib/client.dart b/packages/web_benchmarks/lib/client.dart index 6676dc8f6ba9..43ef711918cd 100644 --- a/packages/web_benchmarks/lib/client.dart +++ b/packages/web_benchmarks/lib/client.dart @@ -30,8 +30,6 @@ final LocalBenchmarkServerClient _client = LocalBenchmarkServerClient(); /// When used without a server, prompts the user to select a benchmark to /// run next. Future runBenchmarks(Map benchmarks) async { - assert(benchmarks != null); - // Set local benchmarks. _benchmarks = benchmarks; diff --git a/packages/web_benchmarks/lib/src/benchmark_result.dart b/packages/web_benchmarks/lib/src/benchmark_result.dart index f4326291fbf8..125bc2cd4ec9 100644 --- a/packages/web_benchmarks/lib/src/benchmark_result.dart +++ b/packages/web_benchmarks/lib/src/benchmark_result.dart @@ -10,7 +10,7 @@ class BenchmarkScore { BenchmarkScore({ required this.metric, required this.value, - }) : assert(metric != null && value != null); + }); /// The name of the metric that this score is categorized under. /// diff --git a/packages/web_benchmarks/lib/src/browser.dart b/packages/web_benchmarks/lib/src/browser.dart index 40894ddd4b32..0918730d5cc8 100644 --- a/packages/web_benchmarks/lib/src/browser.dart +++ b/packages/web_benchmarks/lib/src/browser.dart @@ -345,12 +345,6 @@ class BlinkTraceSummary { orElse: () => throw noMeasuredFramesFound(), ); - if (firstMeasuredFrameEvent == null) { - // This happens in benchmarks that do not measure frames, such as some - // of the text layout benchmarks. - return null; - } - final int tabPid = firstMeasuredFrameEvent.pid; // Filter out data from unrelated processes diff --git a/packages/web_benchmarks/lib/src/recorder.dart b/packages/web_benchmarks/lib/src/recorder.dart index 52dc6b8c567e..51fa697cecfd 100644 --- a/packages/web_benchmarks/lib/src/recorder.dart +++ b/packages/web_benchmarks/lib/src/recorder.dart @@ -827,8 +827,7 @@ class Profile { /// /// [name] and [useCustomWarmUp] must not be null. Profile({required this.name, this.useCustomWarmUp = false}) - : assert(name != null), - _isWarmingUp = useCustomWarmUp; + : _isWarmingUp = useCustomWarmUp; /// The name of the benchmark that produced this profile. final String name; @@ -1213,13 +1212,6 @@ final Map _engineBenchmarkListeners = /// If another listener is already registered, overrides it. void registerEngineBenchmarkValueListener( String name, EngineBenchmarkValueListener listener) { - if (listener == null) { - throw ArgumentError( - 'Listener must not be null. To stop listening to engine benchmark values ' - 'under label "$name", call stopListeningToEngineBenchmarkValues(\'$name\').', - ); - } - if (_engineBenchmarkListeners.containsKey(name)) { throw StateError('A listener for "$name" is already registered.\n' 'Call `stopListeningToEngineBenchmarkValues` to unregister the previous ' diff --git a/packages/web_benchmarks/lib/src/runner.dart b/packages/web_benchmarks/lib/src/runner.dart index 7c58e620001c..8ab5d2709b6b 100644 --- a/packages/web_benchmarks/lib/src/runner.dart +++ b/packages/web_benchmarks/lib/src/runner.dart @@ -278,11 +278,11 @@ class BenchmarkServer { final List scoreKeys = List.from(profile['scoreKeys'] as Iterable); - if (scoreKeys == null || scoreKeys.isEmpty) { + if (scoreKeys.isEmpty) { throw StateError('No score keys in benchmark "$benchmarkName"'); } for (final String scoreKey in scoreKeys) { - if (scoreKey == null || scoreKey.isEmpty) { + if (scoreKey.isEmpty) { throw StateError( 'Score key is empty in benchmark "$benchmarkName". ' 'Received [${scoreKeys.join(', ')}]'); diff --git a/packages/web_benchmarks/pubspec.yaml b/packages/web_benchmarks/pubspec.yaml index a95f7dafdfce..18c833792ba3 100644 --- a/packages/web_benchmarks/pubspec.yaml +++ b/packages/web_benchmarks/pubspec.yaml @@ -2,11 +2,11 @@ name: web_benchmarks description: A benchmark harness for performance-testing Flutter apps in Chrome. repository: https://github.com/flutter/packages/tree/main/packages/web_benchmarks issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+web_benchmarks%22 -version: 0.1.0+3 +version: 0.1.0+4 environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/web_benchmarks/testing/test_app/pubspec.yaml b/packages/web_benchmarks/testing/test_app/pubspec.yaml index d3b36783be12..da96462e3cb9 100644 --- a/packages/web_benchmarks/testing/test_app/pubspec.yaml +++ b/packages/web_benchmarks/testing/test_app/pubspec.yaml @@ -6,7 +6,7 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=2.18.0 <4.0.0" dependencies: cupertino_icons: ^1.0.5 diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 3b05257195c2..bf535d2b1a9d 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.2.1 + +* Removes obsolete null checks on non-nullable values. + ## 4.2.0 * Adds support to receive permission requests. See `WebViewController(onPermissionRequest)`. diff --git a/packages/webview_flutter/webview_flutter/example/.pluginToolsConfig.yaml b/packages/webview_flutter/webview_flutter/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index 9cbfc01de56e..c7fbd63958fd 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -440,7 +440,7 @@ class SampleMenu extends StatelessWidget { } Widget _getCookieList(String cookies) { - if (cookies == null || cookies == '""') { + if (cookies == '""') { return Container(); } final List cookieList = cookies.split(';'); diff --git a/packages/webview_flutter/webview_flutter/lib/src/legacy/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/legacy/webview.dart index 06ba026cada4..55826a533b17 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/legacy/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/legacy/webview.dart @@ -98,9 +98,7 @@ class WebView extends StatefulWidget { AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.allowsInlineMediaPlayback = false, this.backgroundColor, - }) : assert(javascriptMode != null), - assert(initialMediaPlaybackPolicy != null), - assert(allowsInlineMediaPlayback != null); + }); static WebViewPlatform? _platform; @@ -395,11 +393,9 @@ WebSettings _clearUnchangedWebSettings( assert(currentValue.hasNavigationDelegate != null); assert(currentValue.hasProgressTracking != null); assert(currentValue.debuggingEnabled != null); - assert(currentValue.userAgent != null); assert(newValue.javascriptMode != null); assert(newValue.hasNavigationDelegate != null); assert(newValue.debuggingEnabled != null); - assert(newValue.userAgent != null); assert(newValue.zoomEnabled != null); JavascriptMode? javascriptMode; @@ -500,7 +496,7 @@ class WebViewController { this._widget, this._webViewPlatformController, this._javascriptChannelRegistry, - ) : assert(_webViewPlatformController != null) { + ) { _settings = _webSettingsFromWidget(_widget); } @@ -561,7 +557,6 @@ class WebViewController { String url, { Map? headers, }) async { - assert(url != null); _validateUrlString(url); return _webViewPlatformController.loadUrl(url, headers); } diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 05d6cd29bd7c..b931c9e04fd4 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.2.0 +version: 4.2.1 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 82dbbd49f215..3160b8594f6e 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,12 @@ +## 3.7.1 + +* Removes obsolete null checks on non-nullable values. + +## 3.7.0 + +* Adds support to accept third party cookies. See + `AndroidWebViewCookieManager.setAcceptThirdPartyCookies`. + ## 3.6.3 * Updates gradle, AGP and fixes some lint errors. diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java index 9cd785fc33f2..6fdaeeaded04 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java @@ -7,22 +7,93 @@ import android.os.Build; import android.webkit.CookieManager; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.CookieManagerHostApi; +import java.util.Objects; + +/** + * Host API implementation for `CookieManager`. + * + *

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 CookieManagerHostApiImpl implements CookieManagerHostApi { + // To ease adding additional methods, this value is added prematurely. + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final BinaryMessenger binaryMessenger; + + private final InstanceManager instanceManager; + private final CookieManagerProxy proxy; + + /** Proxy for constructors and static method of `CookieManager`. */ + @VisibleForTesting + static class CookieManagerProxy { + /** Handles the Dart static method `MyClass.myStaticMethod`. */ + @NonNull + public CookieManager getInstance() { + return CookieManager.getInstance(); + } + } + + /** + * Constructs a {@link CookieManagerHostApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public CookieManagerHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this(binaryMessenger, instanceManager, new CookieManagerProxy()); + } + + /** + * Constructs a {@link CookieManagerHostApiImpl}. + * + * @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 methods of `CookieManager` + */ + public CookieManagerHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, + @NonNull InstanceManager instanceManager, + @NonNull CookieManagerProxy proxy) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + this.proxy = proxy; + } -class CookieManagerHostApiImpl implements CookieManagerHostApi { @Override - public void clearCookies(@NonNull GeneratedAndroidWebView.Result result) { - CookieManager cookieManager = CookieManager.getInstance(); + public void attachInstance(@NonNull Long instanceIdentifier) { + instanceManager.addDartCreatedInstance(proxy.getInstance(), instanceIdentifier); + } + + @Override + public void setCookie(@NonNull Long identifier, @NonNull String url, @NonNull String value) { + getCookieManagerInstance(identifier).setCookie(url, value); + } + + @Override + public void removeAllCookies( + @NonNull Long identifier, @NonNull GeneratedAndroidWebView.Result result) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookies(result::success); + getCookieManagerInstance(identifier).removeAllCookies(result::success); } else { - result.success(removeCookiesPreL(cookieManager)); + result.success(removeCookiesPreL(getCookieManagerInstance(identifier))); } } @Override - public void setCookie(@NonNull String url, @NonNull String value) { - CookieManager.getInstance().setCookie(url, value); + public void setAcceptThirdPartyCookies( + @NonNull Long identifier, @NonNull Long webViewIdentifier, @NonNull Boolean accept) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getCookieManagerInstance(identifier) + .setAcceptThirdPartyCookies( + Objects.requireNonNull(instanceManager.getInstance(webViewIdentifier)), accept); + } else { + throw new UnsupportedOperationException( + "`setAcceptThirdPartyCookies` is unsupported on versions below `Build.VERSION_CODES.LOLLIPOP`."); + } } /** @@ -40,4 +111,9 @@ private boolean removeCookiesPreL(CookieManager cookieManager) { } return hasCookies; } + + @NonNull + private CookieManager getCookieManagerInstance(@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/GeneratedAndroidWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java index a9dc42f40103..4dc43fa1d9db 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 @@ -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.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.webviewflutter; @@ -589,12 +589,25 @@ public void dispose(@NonNull Long identifierArg, @NonNull Reply callback) channelReply -> callback.reply(null)); } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + /** + * Host API for `CookieManager`. + * + *

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. + * + *

Generated interface from Pigeon that represents a handler of messages from Flutter. + */ public interface CookieManagerHostApi { - - void clearCookies(@NonNull Result result); - - void setCookie(@NonNull String url, @NonNull String value); + /** Handles attaching `CookieManager.instance` to a native instance. */ + void attachInstance(@NonNull Long instanceIdentifier); + /** Handles Dart method `CookieManager.setCookie`. */ + void setCookie(@NonNull Long identifier, @NonNull String url, @NonNull String value); + /** Handles Dart method `CookieManager.removeAllCookies`. */ + void removeAllCookies(@NonNull Long identifier, @NonNull Result result); + /** Handles Dart method `CookieManager.setAcceptThirdPartyCookies`. */ + void setAcceptThirdPartyCookies( + @NonNull Long identifier, @NonNull Long webViewIdentifier, @NonNull Boolean accept); /** The codec used by CookieManagerHostApi. */ static @NonNull MessageCodec getCodec() { @@ -610,12 +623,66 @@ static void setup( BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.CookieManagerHostApi.clearCookies", + "dev.flutter.pigeon.CookieManagerHostApi.attachInstance", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number instanceIdentifierArg = (Number) args.get(0); + try { + api.attachInstance( + (instanceIdentifierArg == null) ? null : instanceIdentifierArg.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.CookieManagerHostApi.setCookie", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + String urlArg = (String) args.get(1); + String valueArg = (String) args.get(2); + try { + api.setCookie( + (identifierArg == null) ? null : identifierArg.longValue(), urlArg, valueArg); + 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.CookieManagerHostApi.removeAllCookies", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); Result resultCallback = new Result() { public void success(Boolean result) { @@ -629,7 +696,8 @@ public void error(Throwable error) { } }; - api.clearCookies(resultCallback); + api.removeAllCookies( + (identifierArg == null) ? null : identifierArg.longValue(), resultCallback); }); } else { channel.setMessageHandler(null); @@ -638,16 +706,22 @@ public void error(Throwable error) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.CookieManagerHostApi.setCookie", getCodec()); + binaryMessenger, + "dev.flutter.pigeon.CookieManagerHostApi.setAcceptThirdPartyCookies", + getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; - String urlArg = (String) args.get(0); - String valueArg = (String) args.get(1); + Number identifierArg = (Number) args.get(0); + Number webViewIdentifierArg = (Number) args.get(1); + Boolean acceptArg = (Boolean) args.get(2); try { - api.setCookie(urlArg, valueArg); + api.setAcceptThirdPartyCookies( + (identifierArg == null) ? null : identifierArg.longValue(), + (webViewIdentifierArg == null) ? null : webViewIdentifierArg.longValue(), + acceptArg); wrapped.add(0, null); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); 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 af34649211ab..79640b90b0b4 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 @@ -127,7 +127,8 @@ private void setUp( instanceManager, new WebSettingsHostApiImpl.WebSettingsCreator())); FlutterAssetManagerHostApi.setup( binaryMessenger, new FlutterAssetManagerHostApiImpl(flutterAssetManager)); - CookieManagerHostApi.setup(binaryMessenger, new CookieManagerHostApiImpl()); + CookieManagerHostApi.setup( + binaryMessenger, new CookieManagerHostApiImpl(binaryMessenger, instanceManager)); WebStorageHostApi.setup( binaryMessenger, new WebStorageHostApiImpl(instanceManager, new WebStorageHostApiImpl.WebStorageCreator())); diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java deleted file mode 100644 index 11cdddfd3332..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java +++ /dev/null @@ -1,87 +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. - -package io.flutter.plugins.webviewflutter; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.os.Build; -import android.webkit.CookieManager; -import android.webkit.ValueCallback; -import io.flutter.plugins.webviewflutter.utils.TestUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.MockedStatic; - -public class CookieManagerHostApiImplTest { - - private CookieManager cookieManager; - private MockedStatic staticMockCookieManager; - - @Before - public void setup() { - staticMockCookieManager = mockStatic(CookieManager.class); - cookieManager = mock(CookieManager.class); - when(CookieManager.getInstance()).thenReturn(cookieManager); - when(cookieManager.hasCookies()).thenReturn(true); - doAnswer( - answer -> { - ValueCallback callback = answer.getArgument(0); - (callback).onReceiveValue(true); - return null; - }) - .when(cookieManager) - .removeAllCookies(any()); - } - - @After - public void tearDown() { - staticMockCookieManager.close(); - } - - @Test - public void setCookieShouldCallSetCookie() { - // Setup - CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); - // Run - impl.setCookie("flutter.dev", "foo=bar; path=/"); - // Verify - verify(cookieManager).setCookie("flutter.dev", "foo=bar; path=/"); - } - - @Test - public void clearCookiesShouldCallRemoveAllCookiesOnAndroidLAbove() { - // Setup - TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.LOLLIPOP); - @SuppressWarnings("unchecked") - GeneratedAndroidWebView.Result result = mock(GeneratedAndroidWebView.Result.class); - CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); - // Run - impl.clearCookies(result); - // Verify - verify(cookieManager).removeAllCookies(any()); - verify(result).success(true); - } - - @Test - @SuppressWarnings("deprecation") - public void clearCookiesShouldCallRemoveAllCookieBelowAndroidL() { - // Setup - TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.KITKAT_WATCH); - @SuppressWarnings("unchecked") - GeneratedAndroidWebView.Result result = mock(GeneratedAndroidWebView.Result.class); - CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); - // Run - impl.clearCookies(result); - // Verify - verify(cookieManager).removeAllCookie(); - verify(result).success(true); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerTest.java new file mode 100644 index 000000000000..5c1abd199913 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerTest.java @@ -0,0 +1,129 @@ +// 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.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Build; +import android.webkit.CookieManager; +import android.webkit.ValueCallback; +import android.webkit.WebView; +import androidx.annotation.NonNull; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.utils.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class CookieManagerTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock public CookieManager mockCookieManager; + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public CookieManagerHostApiImpl.CookieManagerProxy mockProxy; + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Test + public void getInstance() { + final CookieManager mockCookieManager = mock(CookieManager.class); + final long instanceIdentifier = 1; + + when(mockProxy.getInstance()).thenReturn(mockCookieManager); + + final CookieManagerHostApiImpl hostApi = + new CookieManagerHostApiImpl(mockBinaryMessenger, instanceManager, mockProxy); + hostApi.attachInstance(instanceIdentifier); + + assertEquals(instanceManager.getInstance(instanceIdentifier), mockCookieManager); + } + + @Test + public void setCookie() { + final String url = "testString"; + final String value = "testString2"; + + final long instanceIdentifier = 0; + instanceManager.addDartCreatedInstance(mockCookieManager, instanceIdentifier); + + final CookieManagerHostApiImpl hostApi = + new CookieManagerHostApiImpl(mockBinaryMessenger, instanceManager); + + hostApi.setCookie(instanceIdentifier, url, value); + + verify(mockCookieManager).setCookie(url, value); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Test + public void clearCookies() { + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.LOLLIPOP); + + final long instanceIdentifier = 0; + instanceManager.addDartCreatedInstance(mockCookieManager, instanceIdentifier); + + final CookieManagerHostApiImpl hostApi = + new CookieManagerHostApiImpl(mockBinaryMessenger, instanceManager); + + final Boolean[] successResult = new Boolean[1]; + hostApi.removeAllCookies( + instanceIdentifier, + new GeneratedAndroidWebView.Result() { + @Override + public void success(Boolean result) { + successResult[0] = result; + } + + @Override + public void error(@NonNull Throwable error) {} + }); + + final ArgumentCaptor valueCallbackArgumentCaptor = + ArgumentCaptor.forClass(ValueCallback.class); + verify(mockCookieManager).removeAllCookies(valueCallbackArgumentCaptor.capture()); + + final Boolean returnValue = true; + valueCallbackArgumentCaptor.getValue().onReceiveValue(returnValue); + + assertEquals(successResult[0], returnValue); + } + + @Test + public void setAcceptThirdPartyCookies() { + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.LOLLIPOP); + + final WebView mockWebView = mock(WebView.class); + final long webViewIdentifier = 4; + instanceManager.addDartCreatedInstance(mockWebView, webViewIdentifier); + + final boolean accept = true; + + final long instanceIdentifier = 0; + instanceManager.addDartCreatedInstance(mockCookieManager, instanceIdentifier); + + final CookieManagerHostApiImpl hostApi = + new CookieManagerHostApiImpl(mockBinaryMessenger, instanceManager); + + hostApi.setAcceptThirdPartyCookies(instanceIdentifier, webViewIdentifier, accept); + + verify(mockCookieManager).setAcceptThirdPartyCookies(mockWebView, accept); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/.pluginToolsConfig.yaml b/packages/webview_flutter/webview_flutter_android/example/.pluginToolsConfig.yaml new file mode 100644 index 000000000000..3b6017b7609a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/.pluginToolsConfig.yaml @@ -0,0 +1,4 @@ +buildFlags: + _pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/legacy/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/legacy/web_view.dart index bd5778675c25..909056f4ea9c 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/legacy/web_view.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/legacy/web_view.dart @@ -81,9 +81,7 @@ class WebView extends StatefulWidget { AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.allowsInlineMediaPlayback = false, this.backgroundColor, - }) : assert(javascriptMode != null), - assert(initialMediaPlaybackPolicy != null), - assert(allowsInlineMediaPlayback != null); + }); /// The WebView platform that's used by this WebView. /// @@ -366,7 +364,7 @@ class WebViewController { this._widget, this._webViewPlatformController, this._javascriptChannelRegistry, - ) : assert(_webViewPlatformController != null) { + ) { _settings = _webSettingsFromWidget(_widget); } @@ -420,7 +418,6 @@ class WebViewController { String url, { Map? headers, }) async { - assert(url != null); _validateUrlString(url); return _webViewPlatformController.loadUrl(url, headers); } @@ -607,11 +604,9 @@ class WebViewController { assert(currentValue.hasNavigationDelegate != null); assert(currentValue.hasProgressTracking != null); assert(currentValue.debuggingEnabled != null); - assert(currentValue.userAgent != null); assert(newValue.javascriptMode != null); assert(newValue.hasNavigationDelegate != null); assert(newValue.debuggingEnabled != null); - assert(newValue.userAgent != null); assert(newValue.zoomEnabled != null); JavascriptMode? javascriptMode; 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 a8714d3ae3b8..b618c8f3e2cc 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -441,7 +441,7 @@ class SampleMenu extends StatelessWidget { } Widget _getCookieList(String cookies) { - if (cookies == null || cookies == '""') { + if (cookies == '""') { return Container(); } final List cookieList = cookies.split(';'); 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 8182a42f4b7a..6cbb932532aa 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 @@ -51,7 +51,7 @@ class AndroidWebViewProxy { android_webview.WebResourceError error, )? onReceivedRequestError, @Deprecated('Only called on Android version < 23.') - void Function( + void Function( android_webview.WebView webView, int errorCode, String description, 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 35e1767dd6b2..fedd731bd6e5 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 @@ -407,21 +407,34 @@ class WebView extends JavaObject { } /// Manages cookies globally for all webviews. -class CookieManager { - CookieManager._(); - - static CookieManager? _instance; +/// +/// See https://developer.android.com/reference/android/webkit/CookieManager. +class CookieManager extends JavaObject { + /// Instantiates a [CookieManager] 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 + CookieManager.detached({super.binaryMessenger, super.instanceManager}) + : _cookieManagerApi = CookieManagerHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); - /// Gets the globally set CookieManager instance. - static CookieManager get instance => _instance ??= CookieManager._(); + static final CookieManager _instance = + CookieManagerHostApiImpl().attachInstanceFromInstances( + CookieManager.detached(), + ); - /// Setter for the singleton value, for testing purposes only. - @visibleForTesting - static set instance(CookieManager value) => _instance = value; + final CookieManagerHostApiImpl _cookieManagerApi; - /// Pigeon Host Api implementation for [CookieManager]. - @visibleForTesting - static CookieManagerHostApi api = CookieManagerHostApi(); + /// Access a static field synchronously. + static CookieManager get instance { + AndroidWebViewFlutterApis.instance.ensureSetUp(); + return _instance; + } /// Sets a single cookie (key-value pair) for the given URL. Any existing /// cookie with the same host, path and name will be replaced with the new @@ -441,12 +454,37 @@ class CookieManager { /// Params: /// url – the URL for which the cookie is to be set /// value – the cookie as a string, using the format of the 'Set-Cookie' HTTP response header - Future setCookie(String url, String value) => api.setCookie(url, value); + Future setCookie(String url, String value) { + return _cookieManagerApi.setCookieFromInstances(this, url, value); + } /// Removes all cookies. /// /// The returned future resolves to true if any cookies were removed. - Future clearCookies() => api.clearCookies(); + Future removeAllCookies() { + return _cookieManagerApi.removeAllCookiesFromInstances(this); + } + + /// Sets whether the WebView should allow third party cookies to be set. + /// + /// Apps that target `Build.VERSION_CODES.KITKAT` or below default to allowing + /// third party cookies. Apps targeting `Build.VERSION_CODES.LOLLIPOP` or + /// later default to disallowing third party cookies. + Future setAcceptThirdPartyCookies(WebView webView, bool accept) { + return _cookieManagerApi.setAcceptThirdPartyCookiesFromInstances( + this, + webView, + accept, + ); + } + + @override + CookieManager copy() { + return CookieManager.detached( + binaryMessenger: _cookieManagerApi.binaryMessenger, + instanceManager: _cookieManagerApi.instanceManager, + ); + } } /// Manages settings state for a [WebView]. 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 7488cd863375..5a2c56ef16ab 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 @@ -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.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 @@ -258,6 +258,11 @@ abstract class JavaObjectFlutterApi { } } +/// Host API for `CookieManager`. +/// +/// 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. class CookieManagerHostApi { /// Constructor for [CookieManagerHostApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default @@ -268,11 +273,60 @@ class CookieManagerHostApi { static const MessageCodec codec = StandardMessageCodec(); - Future clearCookies() async { + /// Handles attaching `CookieManager.instance` to a native instance. + Future attachInstance(int arg_instanceIdentifier) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CookieManagerHostApi.clearCookies', codec, + 'dev.flutter.pigeon.CookieManagerHostApi.attachInstance', codec, binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send(null) as List?; + final List? replyList = + await channel.send([arg_instanceIdentifier]) 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 `CookieManager.setCookie`. + Future setCookie( + int arg_identifier, String arg_url, String arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CookieManagerHostApi.setCookie', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_url, arg_value]) 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 `CookieManager.removeAllCookies`. + Future removeAllCookies(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CookieManagerHostApi.removeAllCookies', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -294,12 +348,16 @@ class CookieManagerHostApi { } } - Future setCookie(String arg_url, String arg_value) async { + /// Handles Dart method `CookieManager.setAcceptThirdPartyCookies`. + Future setAcceptThirdPartyCookies( + int arg_identifier, int arg_webViewIdentifier, bool arg_accept) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CookieManagerHostApi.setCookie', codec, + 'dev.flutter.pigeon.CookieManagerHostApi.setAcceptThirdPartyCookies', + codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_url, arg_value]) as List?; + final List? replyList = await channel + .send([arg_identifier, arg_webViewIdentifier, arg_accept]) + as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', 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 f1010d76f63e..7f7c3427ead6 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 @@ -1075,3 +1075,59 @@ class PermissionRequestFlutterApiImpl implements PermissionRequestFlutterApi { ); } } + +/// Host api implementation for [CookieManager]. +class CookieManagerHostApiImpl extends CookieManagerHostApi { + /// Constructs a [CookieManagerHostApiImpl]. + CookieManagerHostApiImpl({ + 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. + CookieManager attachInstanceFromInstances(CookieManager instance) { + attachInstance(instanceManager.addDartCreatedInstance(instance)); + return instance; + } + + /// Helper method to convert instance ids to objects. + Future setCookieFromInstances( + CookieManager instance, + String url, + String value, + ) { + return setCookie( + instanceManager.getIdentifier(instance)!, + url, + value, + ); + } + + /// Helper method to convert instance ids to objects. + Future removeAllCookiesFromInstances(CookieManager instance) { + return removeAllCookies(instanceManager.getIdentifier(instance)!); + } + + /// Helper method to convert instance ids to objects. + Future setAcceptThirdPartyCookiesFromInstances( + CookieManager instance, + WebView webView, + bool accept, + ) { + return setAcceptThirdPartyCookies( + instanceManager.getIdentifier(instance)!, + instanceManager.getIdentifier(webView)!, + accept, + ); + } +} 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 c2b1e9a466ae..67df53835d85 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 @@ -43,9 +43,8 @@ class AndroidWebViewControllerCreationParams // ignore: avoid_unused_constructor_parameters PlatformWebViewControllerCreationParams params, { @visibleForTesting - AndroidWebViewProxy androidWebViewProxy = const AndroidWebViewProxy(), - @visibleForTesting - android_webview.WebStorage? androidWebStorage, + AndroidWebViewProxy androidWebViewProxy = const AndroidWebViewProxy(), + @visibleForTesting android_webview.WebStorage? androidWebStorage, }) { return AndroidWebViewControllerCreationParams( androidWebViewProxy: androidWebViewProxy, @@ -195,7 +194,7 @@ class AndroidWebViewController extends PlatformWebViewController { static Future enableDebugging( bool enabled, { @visibleForTesting - AndroidWebViewProxy webViewProxy = const AndroidWebViewProxy(), + AndroidWebViewProxy webViewProxy = const AndroidWebViewProxy(), }) { return webViewProxy.setWebContentsDebuggingEnabled(enabled); } @@ -550,7 +549,7 @@ class AndroidJavaScriptChannelParams extends JavaScriptChannelParams { required super.name, required super.onMessageReceived, @visibleForTesting - AndroidWebViewProxy webViewProxy = const AndroidWebViewProxy(), + AndroidWebViewProxy webViewProxy = const AndroidWebViewProxy(), }) : assert(name.isNotEmpty), _javaScriptChannel = webViewProxy.createJavaScriptChannel( name, @@ -575,7 +574,7 @@ class AndroidJavaScriptChannelParams extends JavaScriptChannelParams { AndroidJavaScriptChannelParams.fromJavaScriptChannelParams( JavaScriptChannelParams params, { @visibleForTesting - AndroidWebViewProxy webViewProxy = const AndroidWebViewProxy(), + AndroidWebViewProxy webViewProxy = const AndroidWebViewProxy(), }) : this( name: params.name, onMessageReceived: params.onMessageReceived, @@ -600,10 +599,9 @@ class AndroidWebViewWidgetCreationParams super.layoutDirection, super.gestureRecognizers, this.displayWithHybridComposition = false, + @visibleForTesting InstanceManager? instanceManager, @visibleForTesting - InstanceManager? instanceManager, - @visibleForTesting - this.platformViewsServiceProxy = const PlatformViewsServiceProxy(), + this.platformViewsServiceProxy = const PlatformViewsServiceProxy(), }) : instanceManager = instanceManager ?? android_webview.JavaObject.globalInstanceManager; @@ -801,7 +799,7 @@ class AndroidNavigationDelegateCreationParams // ignore: avoid_unused_constructor_parameters PlatformNavigationDelegateCreationParams params, { @visibleForTesting - AndroidWebViewProxy androidWebViewProxy = const AndroidWebViewProxy(), + AndroidWebViewProxy androidWebViewProxy = const AndroidWebViewProxy(), }) { return AndroidNavigationDelegateCreationParams._( androidWebViewProxy: androidWebViewProxy, diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_cookie_manager.dart index 5174ca576088..0c1be7c178e6 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_cookie_manager.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_cookie_manager.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'android_webview.dart'; +import 'android_webview_controller.dart'; /// Object specifying creation parameters for creating a [AndroidWebViewCookieManager]. /// @@ -47,7 +48,7 @@ class AndroidWebViewCookieManager extends PlatformWebViewCookieManager { @override Future clearCookies() { - return _cookieManager.clearCookies(); + return _cookieManager.removeAllCookies(); } @override @@ -71,4 +72,19 @@ class AndroidWebViewCookieManager extends PlatformWebViewCookieManager { } return true; } + + /// Sets whether the WebView should allow third party cookies to be set. + /// + /// Apps that target `Build.VERSION_CODES.KITKAT` or below default to allowing + /// third party cookies. Apps targeting `Build.VERSION_CODES.LOLLIPOP` or + /// later default to disallowing third party cookies. + Future setAcceptThirdPartyCookies( + AndroidWebViewController controller, + bool accept, + ) { + // ignore: invalid_use_of_visible_for_testing_member + final WebView webView = WebView.api.instanceManager + .getInstanceWithWeakReference(controller.webViewIdentifier)!; + return _cookieManager.setAcceptThirdPartyCookies(webView, accept); + } } diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_cookie_manager.dart b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_cookie_manager.dart index 663a2076b412..a24f625d8787 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_cookie_manager.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_cookie_manager.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/foundation.dart'; // ignore: implementation_imports import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; @@ -9,9 +10,15 @@ import '../android_webview.dart' as android_webview; /// Handles all cookie operations for the current platform. class WebViewAndroidCookieManager extends WebViewCookieManagerPlatform { + /// Constructs a [WebViewAndroidCookieManager]. + WebViewAndroidCookieManager({ + @visibleForTesting android_webview.CookieManager? cookieManager, + }) : _cookieManager = cookieManager ?? android_webview.CookieManager.instance; + + final android_webview.CookieManager _cookieManager; + @override - Future clearCookies() => - android_webview.CookieManager.instance.clearCookies(); + Future clearCookies() => _cookieManager.removeAllCookies(); @override Future setCookie(WebViewCookie cookie) { @@ -19,7 +26,7 @@ class WebViewAndroidCookieManager extends WebViewCookieManagerPlatform { throw ArgumentError( 'The path property for the provided cookie was not given a legal value.'); } - return android_webview.CookieManager.instance.setCookie( + return _cookieManager.setCookie( cookie.domain, '${Uri.encodeComponent(cookie.name)}=${Uri.encodeComponent(cookie.value)}; path=${cookie.path}', ); diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart index 8ae5ab0cecec..ef9545427ef1 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart @@ -22,12 +22,10 @@ class WebViewAndroidWidget extends StatefulWidget { required this.callbacksHandler, required this.javascriptChannelRegistry, required this.onBuildWidget, + @visibleForTesting this.webViewProxy = const WebViewProxy(), @visibleForTesting - this.webViewProxy = const WebViewProxy(), - @visibleForTesting - this.flutterAssetManager = const android_webview.FlutterAssetManager(), - @visibleForTesting - this.webStorage, + this.flutterAssetManager = const android_webview.FlutterAssetManager(), + @visibleForTesting this.webStorage, }); /// Initial parameters used to setup the WebView. @@ -89,12 +87,10 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { required CreationParams creationParams, required this.callbacksHandler, required this.javascriptChannelRegistry, + @visibleForTesting this.webViewProxy = const WebViewProxy(), @visibleForTesting - this.webViewProxy = const WebViewProxy(), - @visibleForTesting - this.flutterAssetManager = const android_webview.FlutterAssetManager(), - @visibleForTesting - android_webview.WebStorage? webStorage, + this.flutterAssetManager = const android_webview.FlutterAssetManager(), + @visibleForTesting android_webview.WebStorage? webStorage, }) : webStorage = webStorage ?? android_webview.WebStorage.instance, assert(creationParams.webSettings?.hasNavigationDelegate != null), super(callbacksHandler) { 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 62a7aa8a25b2..a4886b9bc5c4 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -113,12 +113,29 @@ abstract class JavaObjectFlutterApi { void dispose(int identifier); } -@HostApi() +/// Host API for `CookieManager`. +/// +/// 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. +@HostApi(dartHostTestHandler: 'TestCookieManagerHostApi') abstract class CookieManagerHostApi { + /// Handles attaching `CookieManager.instance` to a native instance. + void attachInstance(int instanceIdentifier); + + /// Handles Dart method `CookieManager.setCookie`. + void setCookie(int identifier, String url, String value); + + /// Handles Dart method `CookieManager.removeAllCookies`. @async - bool clearCookies(); + bool removeAllCookies(int identifier); - void setCookie(String url, String value); + /// Handles Dart method `CookieManager.setAcceptThirdPartyCookies`. + void setAcceptThirdPartyCookies( + int identifier, + int webViewIdentifier, + bool accept, + ); } @HostApi(dartHostTestHandler: 'TestWebViewHostApi') diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index d9053493cd93..b398dfa9413f 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.6.3 +version: 3.7.1 environment: sdk: ">=2.18.0 <4.0.0" 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 accd4db9ad32..6fb230322172 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 @@ -97,7 +97,7 @@ void main() { void Function(android_webview.WebView webView, String url)? onPageStarted, @Deprecated('Only called on Android version < 23.') - void Function( + void Function( android_webview.WebView webView, int errorCode, String description, 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 8e2be2ba48de..e17347a2c0ee 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 @@ -7,19 +7,29 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:webview_flutter_android/src/android_webview.dart' as android_webview; +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/webview_flutter_android.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'android_webview_cookie_manager_test.mocks.dart'; +import 'test_android_webview.g.dart'; -@GenerateMocks([android_webview.CookieManager]) +@GenerateMocks([ + android_webview.CookieManager, + AndroidWebViewController, + TestInstanceManagerHostApi, +]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + test('clearCookies should call android_webview.clearCookies', () async { final android_webview.CookieManager mockCookieManager = MockCookieManager(); - when(mockCookieManager.clearCookies()) + when(mockCookieManager.removeAllCookies()) .thenAnswer((_) => Future.value(true)); final AndroidWebViewCookieManagerCreationParams params = @@ -32,7 +42,7 @@ void main() { .clearCookies(); expect(hasClearedCookies, true); - verify(mockCookieManager.clearCookies()); + verify(mockCookieManager.removeAllCookies()); }); test('setCookie should throw ArgumentError for cookie with invalid path', () { @@ -56,7 +66,7 @@ void main() { }); test( - 'setCookie should call android_webview.csetCookie with properly formatted cookie value', + 'setCookie should call android_webview.setCookie with properly formatted cookie value', () { final android_webview.CookieManager mockCookieManager = MockCookieManager(); final AndroidWebViewCookieManagerCreationParams params = @@ -76,4 +86,39 @@ void main() { 'foo%26=bar%40; path=/', )); }); + + test('setAcceptThirdPartyCookies', () async { + final MockAndroidWebViewController mockController = + MockAndroidWebViewController(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + android_webview.WebView.api = WebViewHostApiImpl( + instanceManager: instanceManager, + ); + final android_webview.WebView webView = android_webview.WebView.detached( + instanceManager: instanceManager, + ); + + const int webViewIdentifier = 4; + instanceManager.addHostCreatedInstance(webView, webViewIdentifier); + + when(mockController.webViewIdentifier).thenReturn(webViewIdentifier); + + final AndroidWebViewCookieManagerCreationParams params = + AndroidWebViewCookieManagerCreationParams + .fromPlatformWebViewCookieManagerCreationParams( + const PlatformWebViewCookieManagerCreationParams()); + + final android_webview.CookieManager mockCookieManager = MockCookieManager(); + + await AndroidWebViewCookieManager( + params, + cookieManager: mockCookieManager, + ).setAcceptThirdPartyCookies(mockController, false); + + verify(mockCookieManager.setAcceptThirdPartyCookies(webView, false)); + + android_webview.WebView.api = WebViewHostApiImpl(); + }); } diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart index d2937d54f92b..9f4aa1dfc706 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart @@ -3,10 +3,17 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i3; +import 'dart:async' as _i5; +import 'dart:ui' as _i4; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_android/src/android_webview.dart' as _i2; +import 'package:webview_flutter_android/src/android_webview_controller.dart' + as _i6; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' + as _i3; + +import 'test_android_webview.g.dart' as _i7; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -19,6 +26,47 @@ import 'package:webview_flutter_android/src/android_webview.dart' as _i2; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +class _FakeCookieManager_0 extends _i1.SmartFake implements _i2.CookieManager { + _FakeCookieManager_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformWebViewControllerCreationParams_1 extends _i1.SmartFake + implements _i3.PlatformWebViewControllerCreationParams { + _FakePlatformWebViewControllerCreationParams_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeObject_2 extends _i1.SmartFake implements Object { + _FakeObject_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeOffset_3 extends _i1.SmartFake implements _i4.Offset { + _FakeOffset_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [CookieManager]. /// /// See the documentation for Mockito's code generation for more information. @@ -28,7 +76,7 @@ class MockCookieManager extends _i1.Mock implements _i2.CookieManager { } @override - _i3.Future setCookie( + _i5.Future setCookie( String? url, String? value, ) => @@ -40,15 +88,389 @@ class MockCookieManager extends _i1.Mock implements _i2.CookieManager { value, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeAllCookies() => (super.noSuchMethod( + Invocation.method( + #removeAllCookies, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future setAcceptThirdPartyCookies( + _i2.WebView? webView, + bool? accept, + ) => + (super.noSuchMethod( + Invocation.method( + #setAcceptThirdPartyCookies, + [ + webView, + accept, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i2.CookieManager copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeCookieManager_0( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.CookieManager); +} + +/// A class which mocks [AndroidWebViewController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAndroidWebViewController extends _i1.Mock + implements _i6.AndroidWebViewController { + MockAndroidWebViewController() { + _i1.throwOnMissingStub(this); + } + + @override + int get webViewIdentifier => (super.noSuchMethod( + Invocation.getter(#webViewIdentifier), + returnValue: 0, + ) as int); + @override + _i3.PlatformWebViewControllerCreationParams get params => (super.noSuchMethod( + Invocation.getter(#params), + returnValue: _FakePlatformWebViewControllerCreationParams_1( + this, + Invocation.getter(#params), + ), + ) as _i3.PlatformWebViewControllerCreationParams); + @override + _i5.Future loadFile(String? absoluteFilePath) => (super.noSuchMethod( + Invocation.method( + #loadFile, + [absoluteFilePath], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadFlutterAsset(String? key) => (super.noSuchMethod( + Invocation.method( + #loadFlutterAsset, + [key], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadHtmlString( + String? html, { + String? baseUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadHtmlString, + [html], + {#baseUrl: baseUrl}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadRequest(_i3.LoadRequestParams? params) => + (super.noSuchMethod( + Invocation.method( + #loadRequest, + [params], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future currentUrl() => (super.noSuchMethod( + Invocation.method( + #currentUrl, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future clearCache() => (super.noSuchMethod( + Invocation.method( + #clearCache, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future clearLocalStorage() => (super.noSuchMethod( + Invocation.method( + #clearLocalStorage, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setPlatformNavigationDelegate( + _i3.PlatformNavigationDelegate? handler) => + (super.noSuchMethod( + Invocation.method( + #setPlatformNavigationDelegate, + [handler], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future runJavaScript(String? javaScript) => (super.noSuchMethod( + Invocation.method( + #runJavaScript, + [javaScript], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future runJavaScriptReturningResult(String? javaScript) => + (super.noSuchMethod( + Invocation.method( + #runJavaScriptReturningResult, + [javaScript], + ), + returnValue: _i5.Future.value(_FakeObject_2( + this, + Invocation.method( + #runJavaScriptReturningResult, + [javaScript], + ), + )), + ) as _i5.Future); + @override + _i5.Future addJavaScriptChannel( + _i3.JavaScriptChannelParams? javaScriptChannelParams) => + (super.noSuchMethod( + Invocation.method( + #addJavaScriptChannel, + [javaScriptChannelParams], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeJavaScriptChannel(String? javaScriptChannelName) => + (super.noSuchMethod( + Invocation.method( + #removeJavaScriptChannel, + [javaScriptChannelName], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future scrollTo( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + x, + y, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future scrollBy( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + x, + y, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future<_i4.Offset> getScrollPosition() => (super.noSuchMethod( + Invocation.method( + #getScrollPosition, + [], + ), + returnValue: _i5.Future<_i4.Offset>.value(_FakeOffset_3( + this, + Invocation.method( + #getScrollPosition, + [], + ), + )), + ) as _i5.Future<_i4.Offset>); + @override + _i5.Future enableZoom(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #enableZoom, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setBackgroundColor(_i4.Color? color) => (super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [color], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setJavaScriptMode(_i3.JavaScriptMode? javaScriptMode) => + (super.noSuchMethod( + Invocation.method( + #setJavaScriptMode, + [javaScriptMode], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setUserAgent(String? userAgent) => (super.noSuchMethod( + Invocation.method( + #setUserAgent, + [userAgent], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setMediaPlaybackRequiresUserGesture(bool? require) => + (super.noSuchMethod( + Invocation.method( + #setMediaPlaybackRequiresUserGesture, + [require], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setTextZoom(int? textZoom) => (super.noSuchMethod( + Invocation.method( + #setTextZoom, + [textZoom], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setOnShowFileSelector( + _i5.Future> Function(_i6.FileSelectorParams)? + onShowFileSelector) => + (super.noSuchMethod( + Invocation.method( + #setOnShowFileSelector, + [onShowFileSelector], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setOnPlatformPermissionRequest( + void Function(_i3.PlatformWebViewPermissionRequest)? + onPermissionRequest) => + (super.noSuchMethod( + Invocation.method( + #setOnPlatformPermissionRequest, + [onPermissionRequest], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i7.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + @override - _i3.Future clearCookies() => (super.noSuchMethod( + void clear() => super.noSuchMethod( Invocation.method( - #clearCookies, + #clear, [], ), - returnValue: _i3.Future.value(false), - ) as _i3.Future); + returnValueForMissingStub: null, + ); } 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 73b54caabc18..922d7c78bbca 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 @@ -17,6 +17,7 @@ import 'test_android_webview.g.dart'; CookieManagerHostApi, DownloadListener, JavaScriptChannel, + TestCookieManagerHostApi, TestDownloadListenerHostApi, TestInstanceManagerHostApi, TestJavaObjectHostApi, @@ -35,8 +36,9 @@ import 'test_android_webview.g.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - // Mocks the call to clear the native InstanceManager. + // Mocks the calls to the native InstanceManager. TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + TestJavaObjectHostApi.setup(MockTestJavaObjectHostApi()); group('Android WebView', () { group('JavaObject', () { @@ -47,10 +49,6 @@ void main() { TestJavaObjectHostApi.setup(mockPlatformHostApi); }); - tearDown(() { - TestJavaObjectHostApi.setup(null); - }); - test('JavaObject.dispose', () async { int? callbackIdentifier; final InstanceManager instanceManager = InstanceManager( @@ -1051,18 +1049,103 @@ void main() { }); group('CookieManager', () { - test('setCookie calls setCookie on CookieManagerHostApi', () { - CookieManager.api = MockCookieManagerHostApi(); - CookieManager.instance.setCookie('foo', 'bar'); - verify(CookieManager.api.setCookie('foo', 'bar')); + tearDown(() { + TestCookieManagerHostApi.setup(null); + }); + + test('instance', () { + final MockTestCookieManagerHostApi mockApi = + MockTestCookieManagerHostApi(); + TestCookieManagerHostApi.setup(mockApi); + + final CookieManager instance = CookieManager.instance; + + verify(mockApi.attachInstance( + JavaObject.globalInstanceManager.getIdentifier(instance), + )); + }); + + test('setCookie', () async { + final MockTestCookieManagerHostApi mockApi = + MockTestCookieManagerHostApi(); + TestCookieManagerHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final CookieManager instance = CookieManager.detached( + instanceManager: instanceManager, + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + const String url = 'testString'; + const String value = 'testString2'; + + await instance.setCookie(url, value); + + verify(mockApi.setCookie(instanceIdentifier, url, value)); }); - test('clearCookies calls clearCookies on CookieManagerHostApi', () { - CookieManager.api = MockCookieManagerHostApi(); - when(CookieManager.api.clearCookies()) - .thenAnswer((_) => Future.value(true)); - CookieManager.instance.clearCookies(); - verify(CookieManager.api.clearCookies()); + test('clearCookies', () async { + final MockTestCookieManagerHostApi mockApi = + MockTestCookieManagerHostApi(); + TestCookieManagerHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final CookieManager instance = CookieManager.detached( + instanceManager: instanceManager, + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + const bool result = true; + when(mockApi.removeAllCookies( + instanceIdentifier, + )).thenAnswer((_) => Future.value(result)); + + expect(await instance.removeAllCookies(), result); + + verify(mockApi.removeAllCookies(instanceIdentifier)); + }); + + test('setAcceptThirdPartyCookies', () async { + final MockTestCookieManagerHostApi mockApi = + MockTestCookieManagerHostApi(); + TestCookieManagerHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final CookieManager instance = CookieManager.detached( + instanceManager: instanceManager, + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + final WebView webView = WebView.detached( + instanceManager: instanceManager, + ); + const int webViewIdentifier = 4; + instanceManager.addHostCreatedInstance(webView, webViewIdentifier); + + const bool accept = true; + + await instance.setAcceptThirdPartyCookies( + webView, + accept, + ); + + verify(mockApi.setAcceptThirdPartyCookies( + instanceIdentifier, + webViewIdentifier, + accept, + )); }); }); 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 58448c063de7..3b55d856d05b 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 @@ -117,15 +117,18 @@ class MockCookieManagerHostApi extends _i1.Mock } @override - _i5.Future clearCookies() => (super.noSuchMethod( + _i5.Future attachInstance(int? arg_instanceIdentifier) => + (super.noSuchMethod( Invocation.method( - #clearCookies, - [], + #attachInstance, + [arg_instanceIdentifier], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setCookie( + int? arg_identifier, String? arg_url, String? arg_value, ) => @@ -133,6 +136,7 @@ class MockCookieManagerHostApi extends _i1.Mock Invocation.method( #setCookie, [ + arg_identifier, arg_url, arg_value, ], @@ -140,6 +144,32 @@ class MockCookieManagerHostApi extends _i1.Mock returnValue: _i5.Future.value(), returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); + @override + _i5.Future removeAllCookies(int? arg_identifier) => (super.noSuchMethod( + Invocation.method( + #removeAllCookies, + [arg_identifier], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future setAcceptThirdPartyCookies( + int? arg_identifier, + int? arg_webViewIdentifier, + bool? arg_accept, + ) => + (super.noSuchMethod( + Invocation.method( + #setAcceptThirdPartyCookies, + [ + arg_identifier, + arg_webViewIdentifier, + arg_accept, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [DownloadListener]. @@ -223,6 +253,67 @@ class MockJavaScriptChannel extends _i1.Mock implements _i2.JavaScriptChannel { ) as _i2.JavaScriptChannel); } +/// A class which mocks [TestCookieManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestCookieManagerHostApi extends _i1.Mock + implements _i6.TestCookieManagerHostApi { + MockTestCookieManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void attachInstance(int? instanceIdentifier) => super.noSuchMethod( + Invocation.method( + #attachInstance, + [instanceIdentifier], + ), + returnValueForMissingStub: null, + ); + @override + void setCookie( + int? identifier, + String? url, + String? value, + ) => + super.noSuchMethod( + Invocation.method( + #setCookie, + [ + identifier, + url, + value, + ], + ), + returnValueForMissingStub: null, + ); + @override + _i5.Future removeAllCookies(int? identifier) => (super.noSuchMethod( + Invocation.method( + #removeAllCookies, + [identifier], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + void setAcceptThirdPartyCookies( + int? identifier, + int? webViewIdentifier, + bool? accept, + ) => + super.noSuchMethod( + Invocation.method( + #setAcceptThirdPartyCookies, + [ + identifier, + webViewIdentifier, + accept, + ], + ), + returnValueForMissingStub: null, + ); +} + /// A class which mocks [TestDownloadListenerHostApi]. /// /// See the documentation for Mockito's code generation for more information. diff --git a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.dart index e4cd61634864..02576d0d653c 100644 --- a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.dart @@ -16,20 +16,20 @@ import 'webview_android_cookie_manager_test.mocks.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - setUp(() { - android_webview.CookieManager.instance = MockCookieManager(); - }); - test('clearCookies should call android_webview.clearCookies', () { - when(android_webview.CookieManager.instance.clearCookies()) + final MockCookieManager mockCookieManager = MockCookieManager(); + when(mockCookieManager.removeAllCookies()) .thenAnswer((_) => Future.value(true)); - WebViewAndroidCookieManager().clearCookies(); - verify(android_webview.CookieManager.instance.clearCookies()); + WebViewAndroidCookieManager( + cookieManager: mockCookieManager, + ).clearCookies(); + verify(mockCookieManager.removeAllCookies()); }); test('setCookie should throw ArgumentError for cookie with invalid path', () { expect( - () => WebViewAndroidCookieManager().setCookie(const WebViewCookie( + () => WebViewAndroidCookieManager(cookieManager: MockCookieManager()) + .setCookie(const WebViewCookie( name: 'foo', value: 'bar', domain: 'flutter.dev', @@ -42,12 +42,13 @@ void main() { test( 'setCookie should call android_webview.csetCookie with properly formatted cookie value', () { - WebViewAndroidCookieManager().setCookie(const WebViewCookie( + final MockCookieManager mockCookieManager = MockCookieManager(); + WebViewAndroidCookieManager(cookieManager: mockCookieManager) + .setCookie(const WebViewCookie( name: 'foo&', value: 'bar@', domain: 'flutter.dev', )); - verify(android_webview.CookieManager.instance - .setCookie('flutter.dev', 'foo%26=bar%40; path=/')); + verify(mockCookieManager.setCookie('flutter.dev', 'foo%26=bar%40; path=/')); }); } diff --git a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart index b306f2c77d6d..864dbaa04aa0 100644 --- a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart @@ -19,6 +19,16 @@ import 'package:webview_flutter_android/src/android_webview.dart' as _i2; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +class _FakeCookieManager_0 extends _i1.SmartFake implements _i2.CookieManager { + _FakeCookieManager_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [CookieManager]. /// /// See the documentation for Mockito's code generation for more information. @@ -44,11 +54,41 @@ class MockCookieManager extends _i1.Mock implements _i2.CookieManager { returnValueForMissingStub: _i3.Future.value(), ) as _i3.Future); @override - _i3.Future clearCookies() => (super.noSuchMethod( + _i3.Future removeAllCookies() => (super.noSuchMethod( Invocation.method( - #clearCookies, + #removeAllCookies, [], ), returnValue: _i3.Future.value(false), ) as _i3.Future); + @override + _i3.Future setAcceptThirdPartyCookies( + _i2.WebView? webView, + bool? accept, + ) => + (super.noSuchMethod( + Invocation.method( + #setAcceptThirdPartyCookies, + [ + webView, + accept, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i2.CookieManager copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeCookieManager_0( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.CookieManager); } 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 fca0b96dd87a..20b084e40c3c 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 @@ -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.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 @@ -85,6 +85,136 @@ abstract class TestJavaObjectHostApi { } } +/// Host API for `CookieManager`. +/// +/// 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. +abstract class TestCookieManagerHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + /// Handles attaching `CookieManager.instance` to a native instance. + void attachInstance(int instanceIdentifier); + + /// Handles Dart method `CookieManager.setCookie`. + void setCookie(int identifier, String url, String value); + + /// Handles Dart method `CookieManager.removeAllCookies`. + Future removeAllCookies(int identifier); + + /// Handles Dart method `CookieManager.setAcceptThirdPartyCookies`. + void setAcceptThirdPartyCookies( + int identifier, int webViewIdentifier, bool accept); + + static void setup(TestCookieManagerHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CookieManagerHostApi.attachInstance', 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.CookieManagerHostApi.attachInstance was null.'); + final List args = (message as List?)!; + final int? arg_instanceIdentifier = (args[0] as int?); + assert(arg_instanceIdentifier != null, + 'Argument for dev.flutter.pigeon.CookieManagerHostApi.attachInstance was null, expected non-null int.'); + api.attachInstance(arg_instanceIdentifier!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CookieManagerHostApi.setCookie', 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.CookieManagerHostApi.setCookie 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.CookieManagerHostApi.setCookie was null, expected non-null int.'); + final String? arg_url = (args[1] as String?); + assert(arg_url != null, + 'Argument for dev.flutter.pigeon.CookieManagerHostApi.setCookie was null, expected non-null String.'); + final String? arg_value = (args[2] as String?); + assert(arg_value != null, + 'Argument for dev.flutter.pigeon.CookieManagerHostApi.setCookie was null, expected non-null String.'); + api.setCookie(arg_identifier!, arg_url!, arg_value!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CookieManagerHostApi.removeAllCookies', 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.CookieManagerHostApi.removeAllCookies 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.CookieManagerHostApi.removeAllCookies was null, expected non-null int.'); + final bool output = await api.removeAllCookies(arg_identifier!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CookieManagerHostApi.setAcceptThirdPartyCookies', + 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.CookieManagerHostApi.setAcceptThirdPartyCookies 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.CookieManagerHostApi.setAcceptThirdPartyCookies was null, expected non-null int.'); + final int? arg_webViewIdentifier = (args[1] as int?); + assert(arg_webViewIdentifier != null, + 'Argument for dev.flutter.pigeon.CookieManagerHostApi.setAcceptThirdPartyCookies was null, expected non-null int.'); + final bool? arg_accept = (args[2] as bool?); + assert(arg_accept != null, + 'Argument for dev.flutter.pigeon.CookieManagerHostApi.setAcceptThirdPartyCookies was null, expected non-null bool.'); + api.setAcceptThirdPartyCookies( + arg_identifier!, arg_webViewIdentifier!, arg_accept!); + return []; + }); + } + } + } +} + class _TestWebViewHostApiCodec extends StandardMessageCodec { const _TestWebViewHostApiCodec(); @override diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index af8135f270b7..a407df5f5a04 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.1 + +* Removes obsolete null checks on non-nullable values. + ## 2.3.0 * Adds support to receive permission requests. See diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/creation_params.dart index 7c3edf3cf8b0..0c33cde7bc63 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/creation_params.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/creation_params.dart @@ -23,7 +23,7 @@ class CreationParams { AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.backgroundColor, this.cookies = const [], - }) : assert(autoMediaPlaybackPolicy != null); + }); /// The initialUrl to load in the webview. /// diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_channel.dart index 49e505ef5e58..e340625f91a3 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_channel.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_channel.dart @@ -17,9 +17,7 @@ class JavascriptChannel { JavascriptChannel({ required this.name, required this.onMessageReceived, - }) : assert(name != null), - assert(onMessageReceived != null), - assert(_validChannelNames.hasMatch(name)); + }) : assert(_validChannelNames.hasMatch(name)); /// The channel's name. /// diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_message.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_message.dart index 8d080452c54a..c3aceec99e56 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_message.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_message.dart @@ -7,7 +7,7 @@ class JavascriptMessage { /// Constructs a JavaScript message object. /// /// The `message` parameter must not be null. - const JavascriptMessage(this.message) : assert(message != null); + const JavascriptMessage(this.message); /// The contents of the message that was sent by the JavaScript code. final String message; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_resource_error.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_resource_error.dart index b61671f0ac45..fdd814310221 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_resource_error.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_resource_error.dart @@ -16,8 +16,7 @@ class WebResourceError { this.domain, this.errorType, this.failingUrl, - }) : assert(errorCode != null), - assert(description != null); + }); /// Raw code of the error from the respective platform. /// diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_settings.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_settings.dart index 102ab10ccea7..91f0768e38cb 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_settings.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_settings.dart @@ -85,7 +85,7 @@ class WebSettings { this.allowsInlineMediaPlayback, this.zoomEnabled, required this.userAgent, - }) : assert(userAgent != null); + }); /// The JavaScript execution mode to be used by the webview. final JavascriptMode? javascriptMode; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index 12c4050cccaf..7e48f6c99290 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.3.0 +version: 2.3.1 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_web/CHANGELOG.md b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md index 38f28aaf222e..658efbec9118 100644 --- a/packages/webview_flutter/webview_flutter_web/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Removes obsolete null checks on non-nullable values. * Updates minimum Flutter version to 3.3. * Aligns Dart and Flutter SDK constraints. diff --git a/packages/webview_flutter/webview_flutter_web/example/lib/legacy/web_view.dart b/packages/webview_flutter/webview_flutter_web/example/lib/legacy/web_view.dart index 7f1eead8f17b..8b390c7565cc 100644 --- a/packages/webview_flutter/webview_flutter_web/example/lib/legacy/web_view.dart +++ b/packages/webview_flutter/webview_flutter_web/example/lib/legacy/web_view.dart @@ -129,7 +129,7 @@ class WebViewController { WebViewController( this._widget, this._webViewPlatformController, - ) : assert(_webViewPlatformController != null) { + ) { _settings = _webSettingsFromWidget(_widget); } @@ -151,7 +151,6 @@ class WebViewController { String url, { Map? headers, }) async { - assert(url != null); _validateUrlString(url); return _webViewPlatformController.loadUrl(url, headers); } @@ -317,11 +316,9 @@ class WebViewController { assert(currentValue.hasNavigationDelegate != null); assert(currentValue.hasProgressTracking != null); assert(currentValue.debuggingEnabled != null); - assert(currentValue.userAgent != null); assert(newValue.javascriptMode != null); assert(newValue.hasNavigationDelegate != null); assert(newValue.debuggingEnabled != null); - assert(newValue.userAgent != null); assert(newValue.zoomEnabled != null); JavascriptMode? javascriptMode; diff --git a/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_controller.dart b/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_controller.dart index 52f93f911e40..daf186191a03 100644 --- a/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_controller.dart @@ -28,7 +28,7 @@ class WebWebViewControllerCreationParams // ignore: avoid_unused_constructor_parameters PlatformWebViewControllerCreationParams params, { @visibleForTesting - HttpRequestFactory httpRequestFactory = const HttpRequestFactory(), + HttpRequestFactory httpRequestFactory = const HttpRequestFactory(), }) : this(httpRequestFactory: httpRequestFactory); static int _nextIFrameId = 0; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 1041b121f489..566f23fb67be 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,20 @@ +## 3.4.4 + +* Removes obsolete null checks on non-nullable values. + +## 3.4.3 + +* Replace `describeEnum` with the `name` getter. + +## 3.4.2 + +* Fixes an exception caused by the `onUrlChange` callback passing a null `NSUrl`. + +## 3.4.1 + +* Fixes internal type conversion error. +* Adds internal unknown enum values to handle api updates. + ## 3.4.0 * Adds support for `PlatformWebViewController.setOnPlatformPermissionRequest`. 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 82bf99c9a7be..f580b6022c5e 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 @@ -155,4 +155,14 @@ - (void)testFWFWKMediaCaptureTypeDataFromWKMediaCaptureType API_AVAILABLE(ios(15 .value, FWFWKMediaCaptureTypeCameraAndMicrophone); } + +- (void)testNSKeyValueChangeKeyConversionReturnsUnknownIfUnrecognized { + XCTAssertEqual( + FWFNSKeyValueChangeKeyEnumDataFromNativeNSKeyValueChangeKey(@"SomeUnknownValue").value, + FWFNSKeyValueChangeKeyEnumUnknown); +} + +- (void)testWKNavigationTypeConversionReturnsUnknownIfUnrecognized { + XCTAssertEqual(FWFWKNavigationTypeFromNativeWKNavigationType(-15), FWFWKNavigationTypeUnknown); +} @end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/legacy/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/legacy/web_view.dart index 8f4d3917de64..afb5f2065616 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/legacy/web_view.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/legacy/web_view.dart @@ -74,9 +74,7 @@ class WebView extends StatefulWidget { AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.allowsInlineMediaPlayback = false, this.backgroundColor, - }) : assert(javascriptMode != null), - assert(initialMediaPlaybackPolicy != null), - assert(allowsInlineMediaPlayback != null); + }); /// The WebView platform that's used by this WebView. static final WebViewPlatform platform = CupertinoWebView(); @@ -308,7 +306,7 @@ class WebViewController { this._widget, this._webViewPlatformController, this._javascriptChannelRegistry, - ) : assert(_webViewPlatformController != null) { + ) { _settings = _webSettingsFromWidget(_widget); } @@ -369,7 +367,6 @@ class WebViewController { String url, { Map? headers, }) async { - assert(url != null); _validateUrlString(url); return _webViewPlatformController.loadUrl(url, headers); } @@ -560,11 +557,9 @@ class WebViewController { assert(currentValue.hasNavigationDelegate != null); assert(currentValue.hasProgressTracking != null); assert(currentValue.debuggingEnabled != null); - assert(currentValue.userAgent != null); assert(newValue.javascriptMode != null); assert(newValue.hasNavigationDelegate != null); assert(newValue.debuggingEnabled != null); - assert(newValue.userAgent != null); JavascriptMode? javascriptMode; bool? hasNavigationDelegate; 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 29c42969b475..8c3f75312943 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -442,7 +442,7 @@ class SampleMenu extends StatelessWidget { } Widget _getCookieList(String cookies) { - if (cookies == null || cookies == '""') { + if (cookies == '""') { return Container(); } final List cookieList = cookies.split(';'); 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 d66767b943a2..005cecb39b43 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h @@ -137,7 +137,7 @@ extern FWFNSErrorData *FWFNSErrorDataFromNativeNSError(NSError *error); * * @param key The data object containing information to create a FWFNSKeyValueChangeKeyEnumData. * - * @return A FWFNSKeyValueChangeKeyEnumData or nil if data could not be converted. + * @return A FWFNSKeyValueChangeKeyEnumData. */ extern FWFNSKeyValueChangeKeyEnumData *FWFNSKeyValueChangeKeyEnumDataFromNativeNSKeyValueChangeKey( NSKeyValueChangeKey key); 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 8b63d8389fb2..20607daf4aa0 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m @@ -210,6 +210,8 @@ WKNavigationActionPolicy FWFNativeWKNavigationActionPolicyFromEnumData( makeWithValue:FWFNSKeyValueChangeKeyEnumNotificationIsPrior]; } else if ([key isEqualToString:NSKeyValueChangeOldKey]) { return [FWFNSKeyValueChangeKeyEnumData makeWithValue:FWFNSKeyValueChangeKeyEnumOldValue]; + } else { + return [FWFNSKeyValueChangeKeyEnumData makeWithValue:FWFNSKeyValueChangeKeyEnumUnknown]; } return nil; @@ -234,6 +236,8 @@ FWFWKNavigationType FWFWKNavigationTypeFromNativeWKNavigationType(WKNavigationTy case WKNavigationTypeOther: return FWFWKNavigationTypeOther; } + + return FWFWKNavigationTypeUnknown; } FWFWKSecurityOriginData *FWFWKSecurityOriginDataFromNativeWKSecurityOrigin( 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 a5c76018a22c..45b1e42a355a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h @@ -43,6 +43,7 @@ typedef NS_ENUM(NSUInteger, FWFNSKeyValueChangeKeyEnum) { FWFNSKeyValueChangeKeyEnumNewValue = 2, FWFNSKeyValueChangeKeyEnumNotificationIsPrior = 3, FWFNSKeyValueChangeKeyEnumOldValue = 4, + FWFNSKeyValueChangeKeyEnumUnknown = 5, }; /// Mirror of WKUserScriptInjectionTime. @@ -143,6 +144,11 @@ typedef NS_ENUM(NSUInteger, FWFWKNavigationType) { /// See /// https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypeother?language=objc. FWFWKNavigationTypeOther = 5, + /// An unknown navigation type. + /// + /// This does not represent an actual value provided by the platform and only + /// indicates a value was provided that isn't currently supported. + FWFWKNavigationTypeUnknown = 6, }; /// Possible permission decisions for device resource access. @@ -186,6 +192,9 @@ typedef NS_ENUM(NSUInteger, FWFWKMediaCaptureType) { /// https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypemicrophone?language=objc. FWFWKMediaCaptureTypeMicrophone = 2, /// An unknown media device. + /// + /// This does not represent an actual value provided by the platform and only + /// indicates a value was provided that isn't currently supported. FWFWKMediaCaptureTypeUnknown = 3, }; 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 9615e7fb6a99..a7010509ef22 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m @@ -61,7 +61,7 @@ - (void)requestMediaCapturePermissionForDelegateWithIdentifier:(FWFUIDelegate *) webView:(WKWebView *)webView origin:(WKSecurityOrigin *)origin frame:(WKFrameInfo *)frame - type:(FWFWKMediaCaptureType)type + type:(WKMediaCaptureType)type completion: (void (^)(WKPermissionDecision))completion API_AVAILABLE(ios(15.0)) { 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 92372f237599..2ce5055d056c 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 @@ -40,6 +40,7 @@ enum NSKeyValueChangeKeyEnum { newValue, notificationIsPrior, oldValue, + unknown, } /// Mirror of WKUserScriptInjectionTime. @@ -136,6 +137,12 @@ enum WKNavigationType { /// /// See https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypeother?language=objc. other, + + /// An unknown navigation type. + /// + /// This does not represent an actual value provided by the platform and only + /// indicates a value was provided that isn't currently supported. + unknown, } /// Possible permission decisions for device resource access. @@ -178,6 +185,9 @@ enum WKMediaCaptureType { microphone, /// An unknown media device. + /// + /// This does not represent an actual value provided by the platform and only + /// indicates a value was provided that isn't currently supported. unknown, } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart index a9bbdc5f104c..1c87b8ba77b2 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart @@ -91,6 +91,12 @@ enum NSKeyValueChangeKey { /// /// See https://developer.apple.com/documentation/foundation/nskeyvaluechangeoldkey?language=objc. oldValue, + + /// An unknown change key. + /// + /// This does not represent an actual value provided by the platform and only + /// indicates a value was provided that isn't currently supported. + unknown, } /// The supported keys in a cookie attributes dictionary. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/legacy/web_kit_webview_widget.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/legacy/web_kit_webview_widget.dart index 65b3a8f78f7f..2c201be4aecf 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/legacy/web_kit_webview_widget.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/legacy/web_kit_webview_widget.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:math'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as path; @@ -316,7 +315,7 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { final NSUrlRequest urlRequest = NSUrlRequest( url: request.uri.toString(), allHttpHeaderFields: request.headers, - httpMethod: describeEnum(request.method), + httpMethod: request.method.name, httpBody: request.body, ); 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 b7975d5198c0..77a49f7b7e64 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 @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:math'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as path; @@ -239,8 +238,8 @@ class WebKitWebViewController extends PlatformWebViewController { final UrlChangeCallback? urlChangeCallback = controller._currentNavigationDelegate?._onUrlChange; if (urlChangeCallback != null) { - final NSUrl url = change[NSKeyValueChangeKey.newValue]! as NSUrl; - urlChangeCallback(UrlChange(url: await url.getAbsoluteString())); + final NSUrl? url = change[NSKeyValueChangeKey.newValue] as NSUrl?; + urlChangeCallback(UrlChange(url: await url?.getAbsoluteString())); } break; } @@ -302,7 +301,7 @@ class WebKitWebViewController extends PlatformWebViewController { return _webView.loadRequest(NSUrlRequest( url: params.uri.toString(), allHttpHeaderFields: params.headers, - httpMethod: describeEnum(params.method), + httpMethod: params.method.name, httpBody: params.body, )); } 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 bb588bd96c99..20d4ee41f4fe 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart @@ -59,6 +59,7 @@ enum NSKeyValueChangeKeyEnum { newValue, notificationIsPrior, oldValue, + unknown, } // TODO(bparrishMines): Enums need be wrapped in a data class because thay can't @@ -191,6 +192,12 @@ enum WKNavigationType { /// /// See https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypeother?language=objc. other, + + /// An unknown navigation type. + /// + /// This does not represent an actual value provided by the platform and only + /// indicates a value was provided that isn't currently supported. + unknown, } /// Possible permission decisions for device resource access. @@ -241,7 +248,7 @@ enum WKMediaCaptureType { /// 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. + /// indicates a value was provided that isn't currently supported. unknown, } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 915be555f56e..c0e47c83d5ff 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.4.0 +version: 3.4.4 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.mocks.dart deleted file mode 100644 index e445f9963e21..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.mocks.dart +++ /dev/null @@ -1,308 +0,0 @@ -// Mocks generated by Mockito 5.4.0 from annotations -// in webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; -import 'dart:ui' as _i6; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' - as _i5; -import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart' as _i3; -import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.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 - -class _FakeWKWebViewConfiguration_0 extends _i1.SmartFake - implements _i2.WKWebViewConfiguration { - _FakeWKWebViewConfiguration_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeUIScrollView_1 extends _i1.SmartFake implements _i3.UIScrollView { - _FakeUIScrollView_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeWKWebView_2 extends _i1.SmartFake implements _i2.WKWebView { - _FakeWKWebView_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [WKWebView]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKWebView extends _i1.Mock implements _i2.WKWebView { - MockWKWebView() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.WKWebViewConfiguration get configuration => (super.noSuchMethod( - Invocation.getter(#configuration), - returnValue: _FakeWKWebViewConfiguration_0( - this, - Invocation.getter(#configuration), - ), - ) as _i2.WKWebViewConfiguration); - @override - _i3.UIScrollView get scrollView => (super.noSuchMethod( - Invocation.getter(#scrollView), - returnValue: _FakeUIScrollView_1( - this, - Invocation.getter(#scrollView), - ), - ) as _i3.UIScrollView); - @override - _i4.Future setUIDelegate(_i2.WKUIDelegate? delegate) => - (super.noSuchMethod( - Invocation.method( - #setUIDelegate, - [delegate], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future setNavigationDelegate(_i2.WKNavigationDelegate? delegate) => - (super.noSuchMethod( - Invocation.method( - #setNavigationDelegate, - [delegate], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future getUrl() => (super.noSuchMethod( - Invocation.method( - #getUrl, - [], - ), - returnValue: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future getEstimatedProgress() => (super.noSuchMethod( - Invocation.method( - #getEstimatedProgress, - [], - ), - returnValue: _i4.Future.value(0.0), - ) as _i4.Future); - @override - _i4.Future loadRequest(_i5.NSUrlRequest? request) => - (super.noSuchMethod( - Invocation.method( - #loadRequest, - [request], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future loadHtmlString( - String? string, { - String? baseUrl, - }) => - (super.noSuchMethod( - Invocation.method( - #loadHtmlString, - [string], - {#baseUrl: baseUrl}, - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future loadFileUrl( - String? url, { - required String? readAccessUrl, - }) => - (super.noSuchMethod( - Invocation.method( - #loadFileUrl, - [url], - {#readAccessUrl: readAccessUrl}, - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future loadFlutterAsset(String? key) => (super.noSuchMethod( - Invocation.method( - #loadFlutterAsset, - [key], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future canGoBack() => (super.noSuchMethod( - Invocation.method( - #canGoBack, - [], - ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); - @override - _i4.Future canGoForward() => (super.noSuchMethod( - Invocation.method( - #canGoForward, - [], - ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); - @override - _i4.Future goBack() => (super.noSuchMethod( - Invocation.method( - #goBack, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future goForward() => (super.noSuchMethod( - Invocation.method( - #goForward, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future reload() => (super.noSuchMethod( - Invocation.method( - #reload, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future getTitle() => (super.noSuchMethod( - Invocation.method( - #getTitle, - [], - ), - returnValue: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future setAllowsBackForwardNavigationGestures(bool? allow) => - (super.noSuchMethod( - Invocation.method( - #setAllowsBackForwardNavigationGestures, - [allow], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future setCustomUserAgent(String? userAgent) => (super.noSuchMethod( - Invocation.method( - #setCustomUserAgent, - [userAgent], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future evaluateJavaScript(String? javaScriptString) => - (super.noSuchMethod( - Invocation.method( - #evaluateJavaScript, - [javaScriptString], - ), - returnValue: _i4.Future.value(), - ) as _i4.Future); - @override - _i2.WKWebView copy() => (super.noSuchMethod( - Invocation.method( - #copy, - [], - ), - returnValue: _FakeWKWebView_2( - this, - Invocation.method( - #copy, - [], - ), - ), - ) as _i2.WKWebView); - @override - _i4.Future setBackgroundColor(_i6.Color? color) => (super.noSuchMethod( - Invocation.method( - #setBackgroundColor, - [color], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future setOpaque(bool? opaque) => (super.noSuchMethod( - Invocation.method( - #setOpaque, - [opaque], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future addObserver( - _i5.NSObject? observer, { - required String? keyPath, - required Set<_i5.NSKeyValueObservingOptions>? options, - }) => - (super.noSuchMethod( - Invocation.method( - #addObserver, - [observer], - { - #keyPath: keyPath, - #options: options, - }, - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future removeObserver( - _i5.NSObject? observer, { - required String? keyPath, - }) => - (super.noSuchMethod( - Invocation.method( - #removeObserver, - [observer], - {#keyPath: keyPath}, - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); -} 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 b4a8439e285b..41900b559a35 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 @@ -1036,6 +1036,56 @@ void main() { expect(urlChange.url, 'https://www.google.com'); }); + test('setPlatformNavigationDelegate onUrlChange to null NSUrl', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + late final void Function( + String keyPath, + NSObject object, + Map change, + ) webViewObserveValue; + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: ( + _, { + void Function( + String keyPath, + NSObject object, + Map change, + )? observeValue, + }) { + webViewObserveValue = observeValue!; + return mockWebView; + }, + ); + + final WebKitNavigationDelegate navigationDelegate = + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams( + webKitProxy: WebKitProxy( + createNavigationDelegate: CapturingNavigationDelegate.new, + createUIDelegate: WKUIDelegate.detached, + ), + ), + ); + + final Completer urlChangeCompleter = Completer(); + navigationDelegate.setOnUrlChange( + (UrlChange change) => urlChangeCompleter.complete(change), + ); + + await controller.setPlatformNavigationDelegate(navigationDelegate); + + webViewObserveValue( + 'URL', + mockWebView, + {NSKeyValueChangeKey.newValue: null}, + ); + + final UrlChange urlChange = await urlChangeCompleter.future; + expect(urlChange.url, isNull); + }); + test('webViewIdentifier', () { final InstanceManager instanceManager = InstanceManager( onWeakReferenceRemoved: (_) {}, diff --git a/packages/xdg_directories/CHANGELOG.md b/packages/xdg_directories/CHANGELOG.md index 638e74763dc5..5c63413ed555 100644 --- a/packages/xdg_directories/CHANGELOG.md +++ b/packages/xdg_directories/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 1.0.0 * Updates version to 1.0 to reflect the level of API stability. diff --git a/packages/xdg_directories/pubspec.yaml b/packages/xdg_directories/pubspec.yaml index fa2d3de43165..edf6517739ff 100644 --- a/packages/xdg_directories/pubspec.yaml +++ b/packages/xdg_directories/pubspec.yaml @@ -5,7 +5,7 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 1.0.0 environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=2.18.0 <4.0.0" platforms: linux: diff --git a/script/configs/allowed_unpinned_deps.yaml b/script/configs/allowed_unpinned_deps.yaml index 7598a99afede..1cf35c8881e2 100644 --- a/script/configs/allowed_unpinned_deps.yaml +++ b/script/configs/allowed_unpinned_deps.yaml @@ -41,6 +41,7 @@ - html - http - intl +- io - js - json_serializable - lints diff --git a/script/configs/exclude_all_packages_app.yaml b/script/configs/exclude_all_packages_app.yaml index ddc9df2b71a6..60e7c7486fc1 100644 --- a/script/configs/exclude_all_packages_app.yaml +++ b/script/configs/exclude_all_packages_app.yaml @@ -6,5 +6,15 @@ # updating multiple packages for a breaking change in a common dependency in # cases where using a relaxed version constraint isn't possible. +# An application cannot depend directly on multiple federated implementations +# of the same plugin for the same platform, which means the app cannot +# directly depend on both camera_android and camera_android_androidx. +# Since camera_android is endorsed, it will be included transitively +# already, so exclude it from the direct dependency list to allow including +# camera_android_androidx to ensure that they don't conflict at build time +# (if they did, it would be impossible to use camera_android_androidx while +# camera_android is endorsed). +- camera_android + # This is a permament entry, as it should never be a direct app dependency. - plugin_platform_interface diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index 3f30d606dcb7..020dd74667d0 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -45,6 +45,7 @@ final Map _dartSdkForFlutterSdk = { Version(3, 0, 5): Version(2, 17, 6), Version(3, 3, 0): Version(2, 18, 0), Version(3, 7, 0): Version(2, 19, 0), + Version(3, 10, 0): Version(3, 0, 0), }; /// Returns the version of the Dart SDK that shipped with the given Flutter diff --git a/script/tool/lib/src/common/file_utils.dart b/script/tool/lib/src/common/file_utils.dart index 3c2f2f18f954..968de011a218 100644 --- a/script/tool/lib/src/common/file_utils.dart +++ b/script/tool/lib/src/common/file_utils.dart @@ -11,10 +11,21 @@ import 'package:file/file.dart'; /// childFileWithSubcomponents(rootDir, ['foo', 'bar', 'baz.txt']) /// creates a File representing /rootDir/foo/bar/baz.txt. File childFileWithSubcomponents(Directory base, List components) { - Directory dir = base; final String basename = components.removeLast(); + return childDirectoryWithSubcomponents(base, components).childFile(basename); +} + +/// Returns a [Directory] created by appending everything in [components] +/// to [base] as subdirectories. +/// +/// Example: +/// childFileWithSubcomponents(rootDir, ['foo', 'bar']) +/// creates a File representing /rootDir/foo/bar/. +Directory childDirectoryWithSubcomponents( + Directory base, List components) { + Directory dir = base; for (final String directoryName in components) { dir = dir.childDirectory(directoryName); } - return dir.childFile(basename); + return dir; } diff --git a/script/tool/lib/src/common/gradle.dart b/script/tool/lib/src/common/gradle.dart index 746536075014..8dea4836f170 100644 --- a/script/tool/lib/src/common/gradle.dart +++ b/script/tool/lib/src/common/gradle.dart @@ -44,12 +44,13 @@ class GradleProject { /// Runs a `gradlew` command with the given parameters. Future runCommand( - String target, { + String task, { + List additionalTasks = const [], List arguments = const [], }) { return processRunner.runAndStream( gradleWrapper.path, - [target, ...arguments], + [task, ...additionalTasks, ...arguments], workingDir: androidDirectory, ); } diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart index fbba75c6116f..bb0960cd1ce4 100644 --- a/script/tool/lib/src/common/package_state_utils.dart +++ b/script/tool/lib/src/common/package_state_utils.dart @@ -194,7 +194,11 @@ bool _isExampleBuildFile(List pathComponents) { pathComponents.contains('gradle.properties') || pathComponents.contains('build.gradle') || pathComponents.contains('Runner.xcodeproj') || + pathComponents.contains('Runner.xcscheme') || + pathComponents.contains('Runner.xcworkspace') || + pathComponents.contains('Podfile') || pathComponents.contains('CMakeLists.txt') || + pathComponents.contains('.pluginToolsConfig.yaml') || pathComponents.contains('pubspec.yaml'); } @@ -207,9 +211,9 @@ Future _isGradleTestDependencyChange(List pathComponents, return false; } final List diff = await git.getDiffContents(targetPath: repoPath); - final RegExp changeLine = RegExp(r'[+-] '); + final RegExp changeLine = RegExp(r'^[+-] '); final RegExp testDependencyLine = - RegExp(r'[+-]\s*(?:androidT|t)estImplementation\s'); + RegExp(r'^[+-]\s*(?:androidT|t)estImplementation\s'); bool foundTestDependencyChange = false; for (final String line in diff) { if (!changeLine.hasMatch(line) || diff --git a/script/tool/lib/src/common/xcode.dart b/script/tool/lib/src/common/xcode.dart index 83f681bcb492..fc0fdbc13361 100644 --- a/script/tool/lib/src/common/xcode.dart +++ b/script/tool/lib/src/common/xcode.dart @@ -42,8 +42,8 @@ class Xcode { final List args = [ _xcodeBuildCommand, ...actions, - if (workspace != null) ...['-workspace', workspace], - if (scheme != null) ...['-scheme', scheme], + ...['-workspace', workspace], + ...['-scheme', scheme], if (configuration != null) ...['-configuration', configuration], ...extraFlags, ]; diff --git a/script/tool/lib/src/create_all_packages_app_command.dart b/script/tool/lib/src/create_all_packages_app_command.dart index fb07e335e0fa..01f37f3a3108 100644 --- a/script/tool/lib/src/create_all_packages_app_command.dart +++ b/script/tool/lib/src/create_all_packages_app_command.dart @@ -2,26 +2,27 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io' as io; - import 'package:file/file.dart'; +import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; +import 'common/file_utils.dart'; import 'common/package_command.dart'; import 'common/process_runner.dart'; import 'common/repository_package.dart'; -const String _outputDirectoryFlag = 'output-dir'; - -const String _projectName = 'all_packages'; +/// The name of the build-all-packages project, as passed to `flutter create`. +@visibleForTesting +const String allPackagesProjectName = 'all_packages'; -const int _exitUpdateMacosPodfileFailed = 3; -const int _exitUpdateMacosPbxprojFailed = 4; -const int _exitGenNativeBuildFilesFailed = 5; +const int _exitFlutterCreateFailed = 3; +const int _exitGenNativeBuildFilesFailed = 4; +const int _exitMissingFile = 5; +const int _exitMissingLegacySource = 6; /// A command to create an application that builds all in a single application. class CreateAllPackagesAppCommand extends PackageCommand { @@ -29,22 +30,29 @@ class CreateAllPackagesAppCommand extends PackageCommand { CreateAllPackagesAppCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - Directory? pluginsRoot, Platform platform = const LocalPlatform(), }) : super(packagesDir, processRunner: processRunner, platform: platform) { - final Directory defaultDir = - pluginsRoot ?? packagesDir.fileSystem.currentDirectory; argParser.addOption(_outputDirectoryFlag, - defaultsTo: defaultDir.path, - help: - 'The path the directory to create the "$_projectName" project in.\n' + defaultsTo: packagesDir.parent.path, + help: 'The path the directory to create the "$allPackagesProjectName" ' + 'project in.\n' 'Defaults to the repository root.'); + argParser.addOption(_legacySourceFlag, + help: 'A partial project directory to use as a source for replacing ' + 'portions of the created app. All top-level directories in the ' + 'source will replace the corresponding directories in the output ' + 'directory post-create.\n\n' + 'The replacement will be done before any tool-driven ' + 'modifications.'); } + static const String _legacySourceFlag = 'legacy-source'; + static const String _outputDirectoryFlag = 'output-dir'; + /// The location to create the synthesized app project. Directory get _appDirectory => packagesDir.fileSystem .directory(getStringArg(_outputDirectoryFlag)) - .childDirectory(_projectName); + .childDirectory(allPackagesProjectName); /// The synthesized app project. RepositoryPackage get app => RepositoryPackage(_appDirectory); @@ -60,7 +68,15 @@ class CreateAllPackagesAppCommand extends PackageCommand { Future run() async { final int exitCode = await _createApp(); if (exitCode != 0) { - throw ToolExit(exitCode); + printError('Failed to `flutter create`: $exitCode'); + throw ToolExit(_exitFlutterCreateFailed); + } + + final String? legacySource = getNullableStringArg(_legacySourceFlag); + if (legacySource != null) { + final Directory legacyDir = + packagesDir.fileSystem.directory(legacySource); + await _replaceWithLegacy(target: _appDirectory, source: legacyDir); } final Set excluded = getExcludedPackageNames(); @@ -89,7 +105,6 @@ class CreateAllPackagesAppCommand extends PackageCommand { await Future.wait(>[ _updateAppGradle(), - _updateManifest(), _updateMacosPbxproj(), // This step requires the native file generation triggered by // flutter pub get above, so can't currently be run on Windows. @@ -98,80 +113,152 @@ class CreateAllPackagesAppCommand extends PackageCommand { } Future _createApp() async { - final io.ProcessResult result = io.Process.runSync( + return processRunner.runAndStream( flutterCommand, [ 'create', '--template=app', - '--project-name=$_projectName', - '--android-language=java', + '--project-name=$allPackagesProjectName', _appDirectory.path, ], ); - - print(result.stdout); - print(result.stderr); - return result.exitCode; } - Future _updateAppGradle() async { - final File gradleFile = app - .platformDirectory(FlutterPlatform.android) - .childDirectory('app') - .childFile('build.gradle'); - if (!gradleFile.existsSync()) { - throw ToolExit(64); + Future _replaceWithLegacy( + {required Directory target, required Directory source}) async { + if (!source.existsSync()) { + printError('No such legacy source directory: ${source.path}'); + throw ToolExit(_exitMissingLegacySource); + } + for (final FileSystemEntity entity in source.listSync()) { + final String basename = entity.basename; + print('Replacing $basename with legacy version...'); + if (entity is Directory) { + target.childDirectory(basename).deleteSync(recursive: true); + } else { + target.childFile(basename).deleteSync(); + } + _copyDirectory(source: source, target: target); } + } - final StringBuffer newGradle = StringBuffer(); - for (final String line in gradleFile.readAsLinesSync()) { - if (line.contains('minSdkVersion')) { - // minSdkVersion 21 is required by camera_android. - newGradle.writeln('minSdkVersion 21'); - } else if (line.contains('compileSdkVersion')) { - // compileSdkVersion 33 is required by local_auth. - newGradle.writeln('compileSdkVersion 33'); + void _copyDirectory({required Directory target, required Directory source}) { + target.createSync(recursive: true); + for (final FileSystemEntity entity in source.listSync(recursive: true)) { + final List subcomponents = + p.split(p.relative(entity.path, from: source.path)); + if (entity is Directory) { + childDirectoryWithSubcomponents(target, subcomponents) + .createSync(recursive: true); + } else if (entity is File) { + final File targetFile = + childFileWithSubcomponents(target, subcomponents); + targetFile.parent.createSync(recursive: true); + entity.copySync(targetFile.path); } else { - newGradle.writeln(line); + throw UnimplementedError('Unsupported entity: $entity'); } - if (line.contains('defaultConfig {')) { - newGradle.writeln(' multiDexEnabled true'); - } else if (line.contains('dependencies {')) { - // Tests for https://github.com/flutter/flutter/issues/43383 - newGradle.writeln( - " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n", - ); + } + } + + /// Rewrites [file], replacing any lines contain a key in [replacements] with + /// the lines in the corresponding value, and adding any lines in [additions]' + /// values after lines containing the key. + void _adjustFile( + File file, { + Map> replacements = const >{}, + Map> additions = const >{}, + Map> regexReplacements = + const >{}, + }) { + if (replacements.isEmpty && additions.isEmpty) { + return; + } + if (!file.existsSync()) { + printError('Unable to find ${file.path} for updating.'); + throw ToolExit(_exitMissingFile); + } + + final StringBuffer output = StringBuffer(); + for (final String line in file.readAsLinesSync()) { + List? replacementLines; + for (final MapEntry> replacement + in replacements.entries) { + if (line.contains(replacement.key)) { + replacementLines = replacement.value; + break; + } + } + if (replacementLines == null) { + for (final MapEntry> replacement + in regexReplacements.entries) { + final RegExpMatch? match = replacement.key.firstMatch(line); + if (match != null) { + replacementLines = replacement.value; + break; + } + } + } + (replacementLines ?? [line]).forEach(output.writeln); + + for (final String targetString in additions.keys) { + if (line.contains(targetString)) { + additions[targetString]!.forEach(output.writeln); + } } } - gradleFile.writeAsStringSync(newGradle.toString()); + file.writeAsStringSync(output.toString()); } - Future _updateManifest() async { - final File manifestFile = app + Future _updateAppGradle() async { + final File gradleFile = app .platformDirectory(FlutterPlatform.android) .childDirectory('app') - .childDirectory('src') - .childDirectory('main') - .childFile('AndroidManifest.xml'); - if (!manifestFile.existsSync()) { - throw ToolExit(64); - } + .childFile('build.gradle'); - final StringBuffer newManifest = StringBuffer(); - for (final String line in manifestFile.readAsLinesSync()) { - if (line.contains('package="com.example.$_projectName"')) { - newManifest - ..writeln('package="com.example.$_projectName"') - ..writeln('xmlns:tools="http://schemas.android.com/tools">') - ..writeln() - ..writeln( - '', - ); - } else { - newManifest.writeln(line); - } + // Ensure that there is a dependencies section, so the dependencies addition + // below will work. + final String content = gradleFile.readAsStringSync(); + if (!content.contains('\ndependencies {')) { + gradleFile.writeAsStringSync(''' +$content +dependencies {} +'''); } - manifestFile.writeAsStringSync(newManifest.toString()); + + const String lifecycleDependency = + " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'"; + + _adjustFile( + gradleFile, + replacements: >{ + // minSdkVersion 21 is required by camera_android. + 'minSdkVersion': ['minSdkVersion 21'], + // compileSdkVersion 33 is required by local_auth. + 'compileSdkVersion': ['compileSdkVersion 33'], + }, + additions: >{ + 'defaultConfig {': [' multiDexEnabled true'], + }, + regexReplacements: >{ + // Tests for https://github.com/flutter/flutter/issues/43383 + // Handling of 'dependencies' is more complex since it hasn't been very + // stable across template versions. + // - Handle an empty, collapsed dependencies section. + RegExp(r'^dependencies\s+{\s*}$'): [ + 'dependencies {', + lifecycleDependency, + '}', + ], + // - Handle a normal dependencies section. + RegExp(r'^dependencies\s+{$'): [ + 'dependencies {', + lifecycleDependency, + ], + // - See below for handling of the case where there is no dependencies + // section. + }, + ); } Future _genPubspecWithAllPlugins() async { @@ -190,7 +277,7 @@ class CreateAllPackagesAppCommand extends PackageCommand { final Map pluginDeps = await _getValidPathDependencies(); final Pubspec pubspec = Pubspec( - _projectName, + allPackagesProjectName, description: 'Flutter app containing all 1st party plugins.', version: Version.parse('1.0.0+1'), environment: { @@ -300,23 +387,15 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} return; } - final File podfileFile = + final File podfile = app.platformDirectory(FlutterPlatform.macos).childFile('Podfile'); - if (!podfileFile.existsSync()) { - printError("Can't find Podfile for macOS"); - throw ToolExit(_exitUpdateMacosPodfileFailed); - } - - final StringBuffer newPodfile = StringBuffer(); - for (final String line in podfileFile.readAsLinesSync()) { - if (line.contains('platform :osx')) { + _adjustFile( + podfile, + replacements: >{ // macOS 10.15 is required by in_app_purchase. - newPodfile.writeln("platform :osx, '10.15'"); - } else { - newPodfile.writeln(line); - } - } - podfileFile.writeAsStringSync(newPodfile.toString()); + 'platform :osx': ["platform :osx, '10.15'"], + }, + ); } Future _updateMacosPbxproj() async { @@ -324,20 +403,14 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} .platformDirectory(FlutterPlatform.macos) .childDirectory('Runner.xcodeproj') .childFile('project.pbxproj'); - if (!pbxprojFile.existsSync()) { - printError("Can't find project.pbxproj for macOS"); - throw ToolExit(_exitUpdateMacosPbxprojFailed); - } - - final StringBuffer newPbxproj = StringBuffer(); - for (final String line in pbxprojFile.readAsLinesSync()) { - if (line.contains('MACOSX_DEPLOYMENT_TARGET')) { + _adjustFile( + pbxprojFile, + replacements: >{ // macOS 10.15 is required by in_app_purchase. - newPbxproj.writeln(' MACOSX_DEPLOYMENT_TARGET = 10.15;'); - } else { - newPbxproj.writeln(line); - } - } - pbxprojFile.writeAsStringSync(newPbxproj.toString()); + 'MACOSX_DEPLOYMENT_TARGET': [ + ' MACOSX_DEPLOYMENT_TARGET = 10.15;' + ], + }, + ); } } diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart index 93a832eb0e29..30d4d178d065 100644 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -85,7 +85,8 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { packageName = relativeComponents.removeAt(0); } - if (relativeComponents.last.endsWith('.dart')) { + if (relativeComponents.last.endsWith('.dart') && + !await _changeIsCommentOnly(gitVersionFinder, path)) { _changedDartFiles[packageName] ??= []; _changedDartFiles[packageName]! .add(p.posix.joinAll(relativeComponents)); @@ -196,4 +197,28 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { } return pubspec.version != previousVersion; } + + Future _changeIsCommentOnly( + GitVersionFinder git, String repoPath) async { + final List diff = await git.getDiffContents(targetPath: repoPath); + final RegExp changeLine = RegExp(r'^[+-] '); + // This will not catch /**/-style comments, but false negatives are fine + // (and in practice, we almost never use that comment style in Dart code). + final RegExp commentLine = RegExp(r'^[+-]\s*//'); + bool foundComment = false; + for (final String line in diff) { + if (!changeLine.hasMatch(line) || + line.startsWith('--- ') || + line.startsWith('+++ ')) { + continue; + } + if (!commentLine.hasMatch(line)) { + return false; + } + foundComment = true; + } + // Only return true if a comment change was found, as a fail-safe against + // against having the wrong (e.g., incorrectly empty) diff output. + return foundComment; + } } diff --git a/script/tool/lib/src/gradle_check_command.dart b/script/tool/lib/src/gradle_check_command.dart index 09a9d7a55f22..53da6405beb7 100644 --- a/script/tool/lib/src/gradle_check_command.dart +++ b/script/tool/lib/src/gradle_check_command.dart @@ -3,12 +3,18 @@ // found in the LICENSE file. import 'package:file/file.dart'; +import 'package:meta/meta.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/repository_package.dart'; +/// The lowest `ext.kotlin_version` that example apps are allowed to use. +@visibleForTesting +final Version minKotlinVersion = Version(1, 7, 10); + /// A command to enforce gradle file conventions and best practices. class GradleCheckCommand extends PackageLoopingCommand { /// Creates an instance of the gradle check command. @@ -125,6 +131,9 @@ class GradleCheckCommand extends PackageLoopingCommand { if (!_validateJavacLintConfig(package, lines)) { succeeded = false; } + if (!_validateKotlinVersion(package, lines)) { + succeeded = false; + } return succeeded; } @@ -347,4 +356,26 @@ gradle.projectsEvaluated { } return true; } + + /// Validates whether the given [example] has its Kotlin version set to at + /// least a minimum value, if it is set at all. + bool _validateKotlinVersion( + RepositoryPackage example, List gradleLines) { + final RegExp kotlinVersionRegex = + RegExp(r"ext\.kotlin_version\s*=\s*'([\d.]+)'"); + RegExpMatch? match; + if (gradleLines.any((String line) { + match = kotlinVersionRegex.firstMatch(line); + return match != null; + })) { + final Version version = Version.parse(match!.group(1)!); + if (version < minKotlinVersion) { + printError('build.gradle sets "ext.kotlin_version" to "$version". The ' + 'minimum Kotlin version that can be specified is ' + '$minKotlinVersion, for compatibility with modern dependencies.'); + return false; + } + } + return true; + } } diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart index 83ef037ae6ad..93953871fdbf 100644 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; import 'package:yaml_edit/yaml_edit.dart'; @@ -40,7 +41,10 @@ class MakeDepsPathBasedCommand extends PackageCommand { _targetDependenciesWithNonBreakingUpdatesArg, help: 'Causes all packages that have non-breaking version changes ' 'when compared against the git base to be treated as target ' - 'packages.', + 'packages.\n\nOnly packages with dependency constraints that allow ' + 'the new version of a given target package will be updated. E.g., ' + 'if package A depends on B: ^1.0.0, and B is updated from 2.0.0 to ' + '2.0.1, the dependency on B in A will not become path based.', ); } @@ -65,10 +69,11 @@ class MakeDepsPathBasedCommand extends PackageCommand { @override Future run() async { - final Set targetDependencies = - getBoolArg(_targetDependenciesWithNonBreakingUpdatesArg) - ? await _getNonBreakingUpdatePackages() - : getStringListArg(_targetDependenciesArg).toSet(); + final bool targetByVersion = + getBoolArg(_targetDependenciesWithNonBreakingUpdatesArg); + final Set targetDependencies = targetByVersion + ? await _getNonBreakingUpdatePackages() + : getStringListArg(_targetDependenciesArg).toSet(); if (targetDependencies.isEmpty) { print('No target dependencies; nothing to do.'); @@ -78,13 +83,24 @@ class MakeDepsPathBasedCommand extends PackageCommand { final Map localDependencyPackages = _findLocalPackages(targetDependencies); + // For targeting by version change, find the versions of the target + // dependencies. + final Map localPackageVersions = targetByVersion + ? { + for (final RepositoryPackage package + in localDependencyPackages.values) + package.directory.basename: package.parsePubspec().version + } + : {}; final String repoRootPath = (await gitDir).path; for (final File pubspec in await _getAllPubspecs()) { final String displayPath = p.posix.joinAll( path.split(path.relative(pubspec.absolute.path, from: repoRootPath))); final bool changed = await _addDependencyOverridesIfNecessary( - RepositoryPackage(pubspec.parent), localDependencyPackages); + RepositoryPackage(pubspec.parent), + localDependencyPackages, + localPackageVersions); if (changed) { print(' Modified $displayPath'); } @@ -141,16 +157,33 @@ class MakeDepsPathBasedCommand extends PackageCommand { /// useful for overriding transitive dependencies. Future _addDependencyOverridesIfNecessary( RepositoryPackage package, - Map localDependencies, { + Map localDependencies, + Map versions, { Iterable additionalPackagesToOverride = const {}, }) async { final String pubspecContents = package.pubspecFile.readAsStringSync(); + // Returns true if [dependency] allows a dependency on [version]. Always + // returns true if [version] is null, to err on the side of assuming it + // will apply in cases where we don't have a target version. + bool allowsVersion(Dependency dependency, Version? version) { + return version == null || + dependency is! HostedDependency || + dependency.version.allows(version); + } + // Determine the dependencies to be overridden. final Pubspec pubspec = Pubspec.parse(pubspecContents); final Iterable combinedDependencies = [ - ...pubspec.dependencies.keys, - ...pubspec.devDependencies.keys, + // Filter out any dependencies with version constraint that wouldn't allow + // the target if published. + ...>[ + ...pubspec.dependencies.entries, + ...pubspec.devDependencies.entries, + ] + .where((MapEntry element) => + allowsVersion(element.value, versions[element.key])) + .map((MapEntry entry) => entry.key), ...additionalPackagesToOverride, ]; final List packagesToOverride = combinedDependencies @@ -216,7 +249,7 @@ $dependencyOverridesKey: // example app doesn't. Since integration tests are run in the example app, // it needs the overrides in order for tests to pass. for (final RepositoryPackage example in package.getExamples()) { - _addDependencyOverridesIfNecessary(example, localDependencies, + _addDependencyOverridesIfNecessary(example, localDependencies, versions, additionalPackagesToOverride: packagesToOverride); } @@ -271,15 +304,6 @@ $dependencyOverridesKey: print(' Skipping $packageName; no non-breaking version change.'); continue; } - // TODO(stuartmorgan): Remove this special-casing once this tool checks - // for major version differences relative to the dependencies being - // updated rather than the version change in the PR: - // https://github.com/flutter/flutter/issues/121246 - if (packageName == 'pigeon') { - print(' Skipping $packageName; see ' - 'https://github.com/flutter/flutter/issues/121246'); - continue; - } changedPackages.add(packageName); } return changedPackages; @@ -304,7 +328,7 @@ $dependencyOverridesKey: final Version newVersion = pubspec.version!; if ((newVersion.major > 0 && newVersion.major != previousVersion.major) || (newVersion.major == 0 && newVersion.minor != previousVersion.minor)) { - // Breaking changes aren't targetted since they won't be picked up + // Breaking changes aren't targeted since they won't be picked up // automatically. return false; } diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index f9967ca0523e..f6eb3c164e24 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -268,6 +268,7 @@ this command. } final Iterable examples = plugin.getExamples(); + final String pluginName = plugin.directory.basename; bool ranUnitTests = false; bool ranAnyTests = false; @@ -317,7 +318,15 @@ this command. if (runUnitTests) { print('Running unit tests...'); - final int exitCode = await project.runCommand('testDebugUnitTest'); + const String taskName = 'testDebugUnitTest'; + // Target the unit tests in the app and plugin specifically, to avoid + // transitively running tests in dependencies. If unit tests have + // already run in an earlier example, only run any app-level unit tests. + final List pluginTestTask = [ + if (!ranUnitTests) '$pluginName:$taskName' + ]; + final int exitCode = await project.runCommand('app:$taskName', + additionalTasks: pluginTestTask); if (exitCode != 0) { printError('$exampleName unit tests failed.'); failed = true; diff --git a/script/tool/lib/src/podspec_check_command.dart b/script/tool/lib/src/podspec_check_command.dart index 4cda7210a8ef..dda08eee32be 100644 --- a/script/tool/lib/src/podspec_check_command.dart +++ b/script/tool/lib/src/podspec_check_command.dart @@ -141,6 +141,9 @@ class PodspecCheckCommand extends PackageLoopingCommand { podspecPath, '--configuration=Debug', // Release targets unsupported arm64 simulators. Use Debug to only build against targeted x86_64 simulator devices. '--skip-tests', + // TODO(vashworth): remove allow-warnings when https://github.com/flutter/flutter/issues/125812 is fixed. + // https://github.com/flutter/flutter/issues/125812 + '--allow-warnings', '--use-modular-headers', // Flutter sets use_modular_headers! in its templates. if (libraryLint) '--use-libraries' ]; diff --git a/script/tool/lib/src/publish_command.dart b/script/tool/lib/src/publish_command.dart index e7b3d110c5fa..fb197ccd3abe 100644 --- a/script/tool/lib/src/publish_command.dart +++ b/script/tool/lib/src/publish_command.dart @@ -99,6 +99,11 @@ class PublishCommand extends PackageLoopingCommand { // `flutter_plugin_tools-v0.0.24`. static const String _tagFormat = '%PACKAGE%-v%VERSION%'; + /// Returns the correct path where the pub credential is stored. + @visibleForTesting + late final String credentialsPath = + _getCredentialsPath(platform: platform, path: path); + @override final String name = 'publish'; @@ -383,7 +388,6 @@ Safe to ignore if the package is deleted in this commit. required String tag, required _RemoteInfo remote, }) async { - assert(remote != null && tag != null); if (!getBoolArg(_dryRunFlag)) { final io.ProcessResult result = await (await gitDir).runCommand( ['push', remote.name, tag], @@ -397,13 +401,12 @@ Safe to ignore if the package is deleted in this commit. } void _ensureValidPubCredential() { - final String credentialsPath = _credentialsPath; final File credentialFile = packagesDir.fileSystem.file(credentialsPath); if (credentialFile.existsSync() && credentialFile.readAsStringSync().isNotEmpty) { return; } - final String? credential = io.Platform.environment[_pubCredentialName]; + final String? credential = platform.environment[_pubCredentialName]; if (credential == null) { printError(''' No pub credential available. Please check if `$credentialsPath` is valid. @@ -411,46 +414,51 @@ If running this command on CI, you can set the pub credential content in the $_p '''); throw ToolExit(1); } + credentialFile.createSync(recursive: true); credentialFile.openSync(mode: FileMode.writeOnlyAppend) ..writeStringSync(credential) ..closeSync(); } - - /// Returns the correct path where the pub credential is stored. - @visibleForTesting - static String getCredentialPath() { - return _credentialsPath; - } } /// The path in which pub expects to find its credentials file. -final String _credentialsPath = () { - // This follows the same logic as pub: - // https://github.com/dart-lang/pub/blob/d99b0d58f4059d7bb4ac4616fd3d54ec00a2b5d4/lib/src/system_cache.dart#L34-L43 - String? cacheDir; - final String? pubCache = io.Platform.environment['PUB_CACHE']; - if (pubCache != null) { - cacheDir = pubCache; - } else if (io.Platform.isWindows) { - final String? appData = io.Platform.environment['APPDATA']; +String _getCredentialsPath( + {required Platform platform, required p.Context path}) { + // See https://github.com/dart-lang/pub/blob/master/doc/cache_layout.md#layout + String? configDir; + if (platform.isLinux) { + String? configHome = platform.environment['XDG_CONFIG_HOME']; + if (configHome == null) { + final String? home = platform.environment['HOME']; + if (home == null) { + printError('"HOME" environment variable is not set.'); + } else { + configHome = path.join(home, '.config'); + } + } + if (configHome != null) { + configDir = path.join(configHome, 'dart'); + } + } else if (platform.isWindows) { + final String? appData = platform.environment['APPDATA']; if (appData == null) { printError('"APPDATA" environment variable is not set.'); } else { - cacheDir = p.join(appData, 'Pub', 'Cache'); + configDir = path.join(appData, 'dart'); } - } else { - final String? home = io.Platform.environment['HOME']; + } else if (platform.isMacOS) { + final String? home = platform.environment['HOME']; if (home == null) { printError('"HOME" environment variable is not set.'); } else { - cacheDir = p.join(home, '.pub-cache'); + configDir = path.join(home, 'Library', 'Application Support', 'dart'); } } - if (cacheDir == null) { - printError('Unable to determine pub cache location'); + if (configDir == null) { + printError('Unable to determine pub con location'); throw ToolExit(1); } - return p.join(cacheDir, 'credentials.json'); -}(); + return path.join(configDir, 'pub-credentials.json'); +} diff --git a/script/tool/test/common/file_utils_test.dart b/script/tool/test/common/file_utils_test.dart index 79b804e31ea5..4640aa8f6910 100644 --- a/script/tool/test/common/file_utils_test.dart +++ b/script/tool/test/common/file_utils_test.dart @@ -8,25 +8,51 @@ import 'package:flutter_plugin_tools/src/common/file_utils.dart'; import 'package:test/test.dart'; void main() { - test('works on Posix', () async { - final FileSystem fileSystem = - MemoryFileSystem(); + group('childFileWithSubcomponents', () { + test('works on Posix', () async { + final FileSystem fileSystem = MemoryFileSystem(); - final Directory base = fileSystem.directory('/').childDirectory('base'); - final File file = - childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); + final Directory base = fileSystem.directory('/').childDirectory('base'); + final File file = + childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); - expect(file.absolute.path, '/base/foo/bar/baz.txt'); + expect(file.absolute.path, '/base/foo/bar/baz.txt'); + }); + + test('works on Windows', () async { + final FileSystem fileSystem = + MemoryFileSystem(style: FileSystemStyle.windows); + + final Directory base = + fileSystem.directory(r'C:\').childDirectory('base'); + final File file = + childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); + + expect(file.absolute.path, r'C:\base\foo\bar\baz.txt'); + }); }); - test('works on Windows', () async { - final FileSystem fileSystem = - MemoryFileSystem(style: FileSystemStyle.windows); + group('childDirectoryWithSubcomponents', () { + test('works on Posix', () async { + final FileSystem fileSystem = MemoryFileSystem(); + + final Directory base = fileSystem.directory('/').childDirectory('base'); + final Directory dir = + childDirectoryWithSubcomponents(base, ['foo', 'bar', 'baz']); + + expect(dir.absolute.path, '/base/foo/bar/baz'); + }); + + test('works on Windows', () async { + final FileSystem fileSystem = + MemoryFileSystem(style: FileSystemStyle.windows); - final Directory base = fileSystem.directory(r'C:\').childDirectory('base'); - final File file = - childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); + final Directory base = + fileSystem.directory(r'C:\').childDirectory('base'); + final Directory dir = + childDirectoryWithSubcomponents(base, ['foo', 'bar', 'baz']); - expect(file.absolute.path, r'C:\base\foo\bar\baz.txt'); + expect(dir.absolute.path, r'C:\base\foo\bar\baz'); + }); }); } diff --git a/script/tool/test/common/package_state_utils_test.dart b/script/tool/test/common/package_state_utils_test.dart index 9b6429a084ce..a900c2741237 100644 --- a/script/tool/test/common/package_state_utils_test.dart +++ b/script/tool/test/common/package_state_utils_test.dart @@ -78,8 +78,11 @@ void main() { 'packages/a_plugin/example/android/build.gradle', 'packages/a_plugin/example/android/gradle/wrapper/gradle-wrapper.properties', 'packages/a_plugin/example/ios/Runner.xcodeproj/project.pbxproj', + 'packages/a_plugin/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme', 'packages/a_plugin/example/linux/flutter/CMakeLists.txt', + 'packages/a_plugin/example/macos/Podfile', 'packages/a_plugin/example/macos/Runner.xcodeproj/project.pbxproj', + 'packages/a_plugin/example/macos/Runner.xcworkspace/contents.xcworkspacedata', 'packages/a_plugin/example/windows/CMakeLists.txt', 'packages/a_plugin/example/pubspec.yaml', // Pigeon platform tests, which have an unusual structure. diff --git a/script/tool/test/create_all_packages_app_command_test.dart b/script/tool/test/create_all_packages_app_command_test.dart index c545c1f3f5a0..6f7ba8ead2e3 100644 --- a/script/tool/test/create_all_packages_app_command_test.dart +++ b/script/tool/test/create_all_packages_app_command_test.dart @@ -6,7 +6,7 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; -import 'package:file/local.dart'; +import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/create_all_packages_app_command.dart'; import 'package:platform/platform.dart'; @@ -18,16 +18,15 @@ import 'util.dart'; void main() { late CommandRunner runner; late CreateAllPackagesAppCommand command; + late Platform mockPlatform; late FileSystem fileSystem; late Directory testRoot; late Directory packagesDir; late RecordingProcessRunner processRunner; setUp(() { - // Since the core of this command is a call to 'flutter create', the test - // has to use the real filesystem. Put everything possible in a unique - // temporary to minimize effect on the host system. - fileSystem = const LocalFileSystem(); + mockPlatform = MockPlatform(isMacOS: true); + fileSystem = MemoryFileSystem(); testRoot = fileSystem.systemTempDirectory.createTempSync(); packagesDir = testRoot.childDirectory('packages'); processRunner = RecordingProcessRunner(); @@ -35,34 +34,142 @@ void main() { command = CreateAllPackagesAppCommand( packagesDir, processRunner: processRunner, - pluginsRoot: testRoot, + platform: mockPlatform, ); runner = CommandRunner( 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); runner.addCommand(command); }); - tearDown(() { - testRoot.deleteSync(recursive: true); - }); + /// Simulates enough of `flutter create`s output to allow the modifications + /// made by the command to work. + void writeFakeFlutterCreateOutput( + Directory outputDirectory, { + String dartSdkConstraint = '>=3.0.0 <4.0.0', + String? appBuildGradleDependencies, + bool androidOnly = false, + }) { + final RepositoryPackage package = RepositoryPackage( + outputDirectory.childDirectory(allPackagesProjectName)); + + // Android + final String dependencies = appBuildGradleDependencies ?? + r''' +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} +'''; + package + .platformDirectory(FlutterPlatform.android) + .childDirectory('app') + .childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync(''' +android { + namespace 'dev.flutter.packages.foo.example' + compileSdkVersion flutter.compileSdkVersion + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + defaultConfig { + applicationId "dev.flutter.packages.foo.example" + minSdkVersion flutter.minSdkVersion + targetSdkVersion 32 + } +} + +$dependencies +'''); + + if (androidOnly) { + return; + } + + // Non-platform-specific + package.pubspecFile + ..createSync(recursive: true) + ..writeAsStringSync(''' +name: $allPackagesProjectName +description: Flutter app containing all 1st party plugins. +publish_to: none +version: 1.0.0 + +environment: + sdk: '$dartSdkConstraint' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter +### +'''); + + // macOS + final Directory macOS = package.platformDirectory(FlutterPlatform.macos); + macOS.childDirectory('Runner.xcodeproj').childFile('project.pbxproj') + ..createSync(recursive: true) + ..writeAsStringSync(''' + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + }; + name = Release; + }; +'''); + macOS.childFile('Podfile') + ..createSync(recursive: true) + ..writeAsStringSync(''' +# platform :osx, '10.14' + +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} +'''); + } group('non-macOS host', () { setUp(() { + mockPlatform = MockPlatform(isLinux: true); command = CreateAllPackagesAppCommand( packagesDir, processRunner: processRunner, - // Set isWindows or not based on the actual host, so that - // `flutterCommand` works, since these tests actually call 'flutter'. - // The important thing is that isMacOS always returns false. - platform: MockPlatform(isWindows: const LocalPlatform().isWindows), - pluginsRoot: testRoot, + platform: mockPlatform, ); runner = CommandRunner( 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); runner.addCommand(command); }); + test('calls "flutter create"', () async { + writeFakeFlutterCreateOutput(testRoot); + createFakePlugin('plugina', packagesDir); + + await runCapturingPrint(runner, ['create-all-packages-app']); + + expect( + processRunner.recordedCalls, + contains(ProcessCall( + getFlutterCommand(mockPlatform), + [ + 'create', + '--template=app', + '--project-name=$allPackagesProjectName', + testRoot.childDirectory(allPackagesProjectName).path, + ], + null))); + }); + test('pubspec includes all plugins', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); createFakePlugin('pluginb', packagesDir); createFakePlugin('pluginc', packagesDir); @@ -80,6 +187,7 @@ void main() { }); test('pubspec has overrides for all plugins', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); createFakePlugin('pluginb', packagesDir); createFakePlugin('pluginc', packagesDir); @@ -97,33 +205,186 @@ void main() { ])); }); - test('pubspec preserves existing Dart SDK version', () async { - const String baselineProjectName = 'baseline'; - final Directory baselineProjectDirectory = - testRoot.childDirectory(baselineProjectName); - io.Process.runSync( - getFlutterCommand(const LocalPlatform()), - [ - 'create', - '--template=app', - '--project-name=$baselineProjectName', - baselineProjectDirectory.path, - ], - ); - final Pubspec baselinePubspec = - RepositoryPackage(baselineProjectDirectory).parsePubspec(); + test('legacy files are copied when requested', () async { + writeFakeFlutterCreateOutput(testRoot); + createFakePlugin('plugina', packagesDir); + // Make a fake legacy source with all the necessary files, replacing one + // of them. + final Directory legacyDir = testRoot.childDirectory('legacy'); + final RepositoryPackage legacySource = + RepositoryPackage(legacyDir.childDirectory(allPackagesProjectName)); + writeFakeFlutterCreateOutput(legacyDir, androidOnly: true); + const String legacyAppBuildGradleContents = 'Fake legacy content'; + final File legacyGradleFile = legacySource + .platformDirectory(FlutterPlatform.android) + .childFile('build.gradle'); + legacyGradleFile.writeAsStringSync(legacyAppBuildGradleContents); + + await runCapturingPrint(runner, [ + 'create-all-packages-app', + '--legacy-source=${legacySource.path}', + ]); + final File buildGradle = command.app + .platformDirectory(FlutterPlatform.android) + .childFile('build.gradle'); + + expect(buildGradle.readAsStringSync(), legacyAppBuildGradleContents); + }); + + test('legacy directory replaces, rather than overlaying', () async { + writeFakeFlutterCreateOutput(testRoot); + createFakePlugin('plugina', packagesDir); + final File extraFile = + RepositoryPackage(testRoot.childDirectory(allPackagesProjectName)) + .platformDirectory(FlutterPlatform.android) + .childFile('extra_file'); + extraFile.createSync(recursive: true); + // Make a fake legacy source with all the necessary files, but not + // including the extra file. + final Directory legacyDir = testRoot.childDirectory('legacy'); + final RepositoryPackage legacySource = + RepositoryPackage(legacyDir.childDirectory(allPackagesProjectName)); + writeFakeFlutterCreateOutput(legacyDir, androidOnly: true); + + await runCapturingPrint(runner, [ + 'create-all-packages-app', + '--legacy-source=${legacySource.path}', + ]); + + expect(extraFile.existsSync(), false); + }); + + test('legacy files are modified as needed by the tool', () async { + writeFakeFlutterCreateOutput(testRoot); + createFakePlugin('plugina', packagesDir); + // Make a fake legacy source with all the necessary files, replacing one + // of them. + final Directory legacyDir = testRoot.childDirectory('legacy'); + final RepositoryPackage legacySource = + RepositoryPackage(legacyDir.childDirectory(allPackagesProjectName)); + writeFakeFlutterCreateOutput(legacyDir, androidOnly: true); + const String legacyAppBuildGradleContents = ''' +# This is the legacy file +android { + compileSdkVersion flutter.compileSdkVersion + defaultConfig { + minSdkVersion flutter.minSdkVersion + } +} +'''; + final File legacyGradleFile = legacySource + .platformDirectory(FlutterPlatform.android) + .childDirectory('app') + .childFile('build.gradle'); + legacyGradleFile.writeAsStringSync(legacyAppBuildGradleContents); + + await runCapturingPrint(runner, [ + 'create-all-packages-app', + '--legacy-source=${legacySource.path}', + ]); + + final List buildGradle = command.app + .platformDirectory(FlutterPlatform.android) + .childDirectory('app') + .childFile('build.gradle') + .readAsLinesSync(); + + expect( + buildGradle, + containsAll([ + contains('This is the legacy file'), + contains('minSdkVersion 21'), + contains('compileSdkVersion 33'), + ])); + }); + + test('pubspec preserves existing Dart SDK version', () async { + const String existingSdkConstraint = '>=1.0.0 <99.0.0'; + writeFakeFlutterCreateOutput(testRoot, + dartSdkConstraint: existingSdkConstraint); createFakePlugin('plugina', packagesDir); await runCapturingPrint(runner, ['create-all-packages-app']); final Pubspec generatedPubspec = command.app.parsePubspec(); const String dartSdkKey = 'sdk'; - expect(generatedPubspec.environment?[dartSdkKey], - baselinePubspec.environment?[dartSdkKey]); + expect(generatedPubspec.environment?[dartSdkKey].toString(), + existingSdkConstraint); + }); + + test('Android app gradle is modified as expected', () async { + writeFakeFlutterCreateOutput(testRoot); + createFakePlugin('plugina', packagesDir); + + await runCapturingPrint(runner, ['create-all-packages-app']); + + final List buildGradle = command.app + .platformDirectory(FlutterPlatform.android) + .childDirectory('app') + .childFile('build.gradle') + .readAsLinesSync(); + + expect( + buildGradle, + containsAll([ + contains('minSdkVersion 21'), + contains('compileSdkVersion 33'), + contains('multiDexEnabled true'), + contains('androidx.lifecycle:lifecycle-runtime'), + ])); + }); + + // The template's app/build.gradle does not always have a dependencies + // section; ensure that the dependency is added if there is not one. + test('Android lifecyle dependency is added with no dependencies', () async { + writeFakeFlutterCreateOutput(testRoot, appBuildGradleDependencies: ''); + createFakePlugin('plugina', packagesDir); + + await runCapturingPrint(runner, ['create-all-packages-app']); + + final List buildGradle = command.app + .platformDirectory(FlutterPlatform.android) + .childDirectory('app') + .childFile('build.gradle') + .readAsLinesSync(); + + expect( + buildGradle, + containsAllInOrder([ + equals('dependencies {'), + contains('androidx.lifecycle:lifecycle-runtime'), + equals('}'), + ])); + }); + + // Some versions of the template's app/build.gradle has an empty + // dependencies section; ensure that the dependency is added in that case. + test('Android lifecyle dependency is added with empty dependencies', + () async { + writeFakeFlutterCreateOutput(testRoot, + appBuildGradleDependencies: 'dependencies {}'); + createFakePlugin('plugina', packagesDir); + + await runCapturingPrint(runner, ['create-all-packages-app']); + + final List buildGradle = command.app + .platformDirectory(FlutterPlatform.android) + .childDirectory('app') + .childFile('build.gradle') + .readAsLinesSync(); + + expect( + buildGradle, + containsAllInOrder([ + equals('dependencies {'), + contains('androidx.lifecycle:lifecycle-runtime'), + equals('}'), + ])); }); test('macOS deployment target is modified in pbxproj', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); await runCapturingPrint(runner, ['create-all-packages-app']); @@ -141,27 +402,50 @@ void main() { }); test('calls flutter pub get', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); await runCapturingPrint(runner, ['create-all-packages-app']); expect( processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(const LocalPlatform()), - const ['pub', 'get'], - testRoot.childDirectory('all_packages').path), + contains(ProcessCall( + getFlutterCommand(mockPlatform), + const ['pub', 'get'], + testRoot.childDirectory(allPackagesProjectName).path))); + }); + + test('fails if flutter create fails', () async { + writeFakeFlutterCreateOutput(testRoot); + createFakePlugin('plugina', packagesDir); + + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ + FakeProcessInfo(MockProcess(exitCode: 1), ['create']) + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['create-all-packages-app'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Failed to `flutter create`'), ])); - }, - // See comment about Windows in create_all_packages_app_command.dart - skip: io.Platform.isWindows); + }); test('fails if flutter pub get fails', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); - processRunner.mockProcessesForExecutable[ - getFlutterCommand(const LocalPlatform())] = [ + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ + FakeProcessInfo(MockProcess(), ['create']), FakeProcessInfo(MockProcess(exitCode: 1), ['pub', 'get']) ]; Error? commandError; @@ -182,20 +466,22 @@ void main() { skip: io.Platform.isWindows); test('handles --output-dir', () async { - createFakePlugin('plugina', packagesDir); - final Directory customOutputDir = fileSystem.systemTempDirectory.createTempSync(); + writeFakeFlutterCreateOutput(customOutputDir); + createFakePlugin('plugina', packagesDir); + await runCapturingPrint(runner, [ 'create-all-packages-app', '--output-dir=${customOutputDir.path}' ]); expect(command.app.path, - customOutputDir.childDirectory('all_packages').path); + customOutputDir.childDirectory(allPackagesProjectName).path); }); test('logs exclusions', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); createFakePlugin('pluginb', packagesDir); createFakePlugin('pluginc', packagesDir); @@ -219,7 +505,6 @@ void main() { packagesDir, processRunner: processRunner, platform: MockPlatform(isMacOS: true), - pluginsRoot: testRoot, ); runner = CommandRunner( 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); @@ -227,10 +512,11 @@ void main() { }); test('macOS deployment target is modified in Podfile', () async { + writeFakeFlutterCreateOutput(testRoot); createFakePlugin('plugina', packagesDir); final File podfileFile = RepositoryPackage( - command.packagesDir.parent.childDirectory('all_packages')) + command.packagesDir.parent.childDirectory(allPackagesProjectName)) .platformDirectory(FlutterPlatform.macos) .childFile('Podfile'); podfileFile.createSync(recursive: true); diff --git a/script/tool/test/federation_safety_check_command_test.dart b/script/tool/test/federation_safety_check_command_test.dart index 0699dc251c42..ac80173cde15 100644 --- a/script/tool/test/federation_safety_check_command_test.dart +++ b/script/tool/test/federation_safety_check_command_test.dart @@ -202,6 +202,23 @@ void main() { final RepositoryPackage platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); + const String appFacingChanges = ''' +diff --git a/packages/foo/foo/lib/foo.dart b/packages/foo/foo/lib/foo.dart +index abc123..def456 100644 +--- a/packages/foo/foo/lib/foo.dart ++++ b/packages/foo/foo/lib/foo.dart +@@ -51,6 +51,9 @@ Future launchUrl( + return true; + } + ++// This is a new method ++bool foo() => true; ++ + // This in an existing method + void aMethod() { + // Do things. +'''; + final String changedFileOutput = [ appFacing.libDirectory.childFile('foo.dart'), implementation.libDirectory.childFile('foo.dart'), @@ -210,6 +227,12 @@ void main() { ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: changedFileOutput)), + // Ensure that a change with both a comment and non-comment addition is + // counted, to validate change analysis. + FakeProcessInfo(MockProcess(stdout: appFacingChanges), + ['', 'HEAD', '--', '/packages/foo/foo/lib/foo.dart']), + // The others diffs don't need to be specified, since empty diff is also + // treated as a non-comment change. ]; Error? commandError; @@ -308,6 +331,141 @@ void main() { ); }); + test('ignores comment-only changes in implementation packages', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final RepositoryPackage implementation = + createFakePlugin('foo_bar', pluginGroupDir); + final RepositoryPackage platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + implementation.libDirectory.childFile('foo.dart'), + platformInterface.pubspecFile, + platformInterface.libDirectory.childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + + const String platformInterfaceChanges = ''' +diff --git a/packages/foo/foo_platform_interface/lib/foo.dart b/packages/foo/foo_platform_interface/lib/foo.dart +index abc123..def456 100644 +--- a/packages/foo/foo_platform_interface/lib/foo.dart ++++ b/packages/foo/foo_platform_interface/lib/foo.dart +@@ -51,6 +51,7 @@ Future launchUrl( + enum Foo { + a, + b, ++ c, + d, + e, + } +'''; + const String implementationChanges = ''' +diff --git a/packages/foo/foo_bar/lib/foo.dart b/packages/foo/foo_bar/lib/foo.dart +index abc123..def456 100644 +--- a/packages/foo/foo_bar/lib/foo.dart ++++ b/packages/foo/foo_bar/lib/foo.dart +@@ -51,6 +51,7 @@ Future launchUrl( + } + + void foo() { ++ // ignore: exhaustive_cases + switch(a_foo) { + case a: + // Do things +'''; + + processRunner.mockProcessesForExecutable['git-diff'] = [ + FakeProcessInfo( + MockProcess(stdout: changedFileOutput), ['--name-only']), + FakeProcessInfo(MockProcess(stdout: implementationChanges), + ['', 'HEAD', '--', '/packages/foo/foo_bar/lib/foo.dart']), + FakeProcessInfo(MockProcess(stdout: platformInterfaceChanges), [ + '', + 'HEAD', + '--', + '/packages/foo/foo_platform_interface/lib/foo.dart' + ]), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo_bar...'), + contains('No Dart changes.'), + ]), + ); + }); + + test('ignores comment-only changes in platform interface packages', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final RepositoryPackage implementation = + createFakePlugin('foo_bar', pluginGroupDir); + final RepositoryPackage platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + implementation.libDirectory.childFile('foo.dart'), + platformInterface.pubspecFile, + platformInterface.libDirectory.childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + + const String platformInterfaceChanges = ''' +diff --git a/packages/foo/foo_platform_interface/lib/foo.dart b/packages/foo/foo_platform_interface/lib/foo.dart +index abc123..def456 100644 +--- a/packages/foo/foo_platform_interface/lib/foo.dart ++++ b/packages/foo/foo_platform_interface/lib/foo.dart +@@ -51,6 +51,8 @@ Future launchUrl( + // existing comment + // existing comment + // existing comment ++ // ++ // additional comment + void foo() { + some code; + } +'''; + const String implementationChanges = ''' +diff --git a/packages/foo/foo_bar/lib/foo.dart b/packages/foo/foo_bar/lib/foo.dart +index abc123..def456 100644 +--- a/packages/foo/foo_bar/lib/foo.dart ++++ b/packages/foo/foo_bar/lib/foo.dart +@@ -51,6 +51,7 @@ Future launchUrl( + } + + void foo() { ++ new code; + existing code; + ... + ... +'''; + + processRunner.mockProcessesForExecutable['git-diff'] = [ + FakeProcessInfo( + MockProcess(stdout: changedFileOutput), ['--name-only']), + FakeProcessInfo(MockProcess(stdout: implementationChanges), + ['', 'HEAD', '--', '/packages/foo/foo_bar/lib/foo.dart']), + FakeProcessInfo(MockProcess(stdout: platformInterfaceChanges), [ + '', + 'HEAD', + '--', + '/packages/foo/foo_platform_interface/lib/foo.dart' + ]), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo_bar...'), + contains('No public code changes for foo_platform_interface.'), + ]), + ); + }); + test('allows things that look like mass changes, with warning', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); diff --git a/script/tool/test/gradle_check_command_test.dart b/script/tool/test/gradle_check_command_test.dart index 68581c7a639a..28603d25cd19 100644 --- a/script/tool/test/gradle_check_command_test.dart +++ b/script/tool/test/gradle_check_command_test.dart @@ -123,6 +123,7 @@ dependencies { RepositoryPackage package, { required String pluginName, required bool warningsConfigured, + String? kotlinVersion, }) { final File buildGradle = package .platformDirectory(FlutterPlatform.android) @@ -140,6 +141,7 @@ gradle.projectsEvaluated { '''; buildGradle.writeAsStringSync(''' buildscript { + ${kotlinVersion == null ? '' : "ext.kotlin_version = '$kotlinVersion'"} repositories { google() mavenCentral() @@ -228,9 +230,12 @@ dependencies { bool includeNamespace = true, bool commentNamespace = false, bool warningsConfigured = true, + String? kotlinVersion, }) { writeFakeExampleTopLevelBuildGradle(package, - pluginName: pluginName, warningsConfigured: warningsConfigured); + pluginName: pluginName, + warningsConfigured: warningsConfigured, + kotlinVersion: kotlinVersion); writeFakeExampleAppBuildGradle(package, includeNamespace: includeNamespace, commentNamespace: commentNamespace); } @@ -644,4 +649,99 @@ dependencies { ], )); }); + + group('Kotlin version check', () { + test('passes if not set', () async { + const String packageName = 'a_package'; + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + writeFakePluginBuildGradle(package, includeLanguageVersion: true); + writeFakeManifest(package); + final RepositoryPackage example = package.getExamples().first; + writeFakeExampleBuildGradles(example, pluginName: packageName); + writeFakeManifest(example, isApp: true); + + final List output = + await runCapturingPrint(runner, ['gradle-check']); + + expect( + output, + containsAllInOrder([ + contains('Validating android/build.gradle'), + ]), + ); + }); + + test('passes if at the minimum allowed version', () async { + const String packageName = 'a_package'; + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + writeFakePluginBuildGradle(package, includeLanguageVersion: true); + writeFakeManifest(package); + final RepositoryPackage example = package.getExamples().first; + writeFakeExampleBuildGradles(example, + pluginName: packageName, kotlinVersion: minKotlinVersion.toString()); + writeFakeManifest(example, isApp: true); + + final List output = + await runCapturingPrint(runner, ['gradle-check']); + + expect( + output, + containsAllInOrder([ + contains('Validating android/build.gradle'), + ]), + ); + }); + + test('passes if above the minimum allowed version', () async { + const String packageName = 'a_package'; + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + writeFakePluginBuildGradle(package, includeLanguageVersion: true); + writeFakeManifest(package); + final RepositoryPackage example = package.getExamples().first; + writeFakeExampleBuildGradles(example, + pluginName: packageName, kotlinVersion: '99.99.0'); + writeFakeManifest(example, isApp: true); + + final List output = + await runCapturingPrint(runner, ['gradle-check']); + + expect( + output, + containsAllInOrder([ + contains('Validating android/build.gradle'), + ]), + ); + }); + + test('fails if below the minimum allowed version', () async { + const String packageName = 'a_package'; + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + writeFakePluginBuildGradle(package, includeLanguageVersion: true); + writeFakeManifest(package); + final RepositoryPackage example = package.getExamples().first; + writeFakeExampleBuildGradles(example, + pluginName: packageName, kotlinVersion: '1.6.21'); + 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 sets "ext.kotlin_version" to "1.6.21". The ' + 'minimum Kotlin version that can be specified is ' + '$minKotlinVersion, for compatibility with modern dependencies.'), + ]), + ); + }); + }); } diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart index 05ca0ca6aab0..1f812005aaa2 100644 --- a/script/tool/test/make_deps_path_based_command_test.dart +++ b/script/tool/test/make_deps_path_based_command_test.dart @@ -48,13 +48,14 @@ void main() { /// Adds dummy 'dependencies:' entries for each package in [dependencies] /// to [package]. - void addDependencies( - RepositoryPackage package, Iterable dependencies) { + void addDependencies(RepositoryPackage package, Iterable dependencies, + {String constraint = '<2.0.0'}) { final List lines = package.pubspecFile.readAsLinesSync(); final int dependenciesStartIndex = lines.indexOf('dependencies:'); assert(dependenciesStartIndex != -1); lines.insertAll(dependenciesStartIndex + 1, [ - for (final String dependency in dependencies) ' $dependency: ^1.0.0', + for (final String dependency in dependencies) + ' $dependency: $constraint', ]); package.pubspecFile.writeAsStringSync(lines.join('\n')); } @@ -62,13 +63,14 @@ void main() { /// Adds a 'dev_dependencies:' section with entries for each package in /// [dependencies] to [package]. void addDevDependenciesSection( - RepositoryPackage package, Iterable devDependencies) { + RepositoryPackage package, Iterable devDependencies, + {String constraint = '<2.0.0'}) { final String originalContent = package.pubspecFile.readAsStringSync(); package.pubspecFile.writeAsStringSync(''' $originalContent dev_dependencies: -${devDependencies.map((String dep) => ' $dep: ^1.0.0').join('\n')} +${devDependencies.map((String dep) => ' $dep: $constraint').join('\n')} '''); } @@ -523,6 +525,87 @@ ${devDependencies.map((String dep) => ' $dep: ^1.0.0').join('\n')} ); }); + test('does not update references with an older major version', () async { + const String newVersion = '2.0.1'; + final RepositoryPackage targetPackage = + createFakePackage('foo', packagesDir, version: newVersion); + final RepositoryPackage referencingPackage = + createFakePackage('bar', packagesDir); + + // For a dependency on ^1.0.0, the 2.0.0->2.0.1 update should not apply. + addDependencies(referencingPackage, ['foo'], + constraint: '^1.0.0'); + + final File pubspecFile = targetPackage.pubspecFile; + final String changedFileOutput = [ + pubspecFile, + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + FakeProcessInfo(MockProcess(stdout: changedFileOutput)), + ]; + final String gitPubspecContents = + pubspecFile.readAsStringSync().replaceAll(newVersion, '2.0.0'); + processRunner.mockProcessesForExecutable['git-show'] = [ + FakeProcessInfo(MockProcess(stdout: gitPubspecContents)), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + final Pubspec referencingPubspec = referencingPackage.parsePubspec(); + + expect( + output, + containsAllInOrder([ + contains('Rewriting references to: foo'), + ]), + ); + expect(referencingPubspec.dependencyOverrides.isEmpty, true); + }); + + test('does update references with a matching version range', () async { + const String newVersion = '2.0.1'; + final RepositoryPackage targetPackage = + createFakePackage('foo', packagesDir, version: newVersion); + final RepositoryPackage referencingPackage = + createFakePackage('bar', packagesDir); + + // For a dependency on ^1.0.0, the 2.0.0->2.0.1 update should not apply. + addDependencies(referencingPackage, ['foo'], + constraint: '^2.0.0'); + + final File pubspecFile = targetPackage.pubspecFile; + final String changedFileOutput = [ + pubspecFile, + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + FakeProcessInfo(MockProcess(stdout: changedFileOutput)), + ]; + final String gitPubspecContents = + pubspecFile.readAsStringSync().replaceAll(newVersion, '2.0.0'); + processRunner.mockProcessesForExecutable['git-show'] = [ + FakeProcessInfo(MockProcess(stdout: gitPubspecContents)), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + final Pubspec referencingPubspec = referencingPackage.parsePubspec(); + + expect( + output, + containsAllInOrder([ + contains('Rewriting references to: foo'), + ]), + ); + expect(referencingPubspec.dependencyOverrides['foo'] is PathDependency, + true); + }); + test('skips anything outside of the packages directory', () async { final Directory toolDir = packagesDir.parent.childDirectory('tool'); const String newVersion = '1.1.0'; diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index 3d327a0e3959..b4e06b9b7e38 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -438,7 +438,10 @@ void main() { orderedEquals([ ProcessCall( androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], + const [ + 'app:testDebugUnitTest', + 'plugin:testDebugUnitTest', + ], androidFolder.path, ), ]), @@ -470,13 +473,62 @@ void main() { orderedEquals([ ProcessCall( androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], + const [ + 'app:testDebugUnitTest', + 'plugin:testDebugUnitTest', + ], androidFolder.path, ), ]), ); }); + test('only runs plugin-level unit tests once', () async { + final RepositoryPackage plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + examples: ['example1', 'example2'], + extraFiles: [ + 'example/example1/android/gradlew', + 'example/example1/android/app/src/test/example_test.java', + 'example/example2/android/gradlew', + 'example/example2/android/app/src/test/example_test.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final List examples = plugin.getExamples().toList(); + final Directory androidFolder1 = + examples[0].platformDirectory(FlutterPlatform.android); + final Directory androidFolder2 = + examples[1].platformDirectory(FlutterPlatform.android); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder1.childFile('gradlew').path, + const [ + 'app:testDebugUnitTest', + 'plugin:testDebugUnitTest', + ], + androidFolder1.path, + ), + ProcessCall( + androidFolder2.childFile('gradlew').path, + const [ + 'app:testDebugUnitTest', + ], + androidFolder2.path, + ), + ]), + ); + }); + test('runs Java integration tests', () async { final RepositoryPackage plugin = createFakePlugin( 'plugin', @@ -629,7 +681,10 @@ public class FlutterActivityTest { orderedEquals([ ProcessCall( androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], + const [ + 'app:testDebugUnitTest', + 'plugin:testDebugUnitTest', + ], androidFolder.path, ), ProcessCall( @@ -708,7 +763,10 @@ public class FlutterActivityTest { orderedEquals([ ProcessCall( androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], + const [ + 'app:testDebugUnitTest', + 'plugin:testDebugUnitTest', + ], androidFolder.path, ), ]), @@ -854,7 +912,7 @@ public class FlutterActivityTest { processRunner.mockProcessesForExecutable[gradlewPath] = [ FakeProcessInfo( - MockProcess(), ['testDebugUnitTest']), // unit passes + MockProcess(), ['app:testDebugUnitTest']), // unit passes FakeProcessInfo(MockProcess(exitCode: 1), ['app:connectedAndroidTest']), // integration fails ]; @@ -1395,8 +1453,13 @@ public class FlutterActivityTest { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], androidFolder.path), + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:testDebugUnitTest', + 'plugin:testDebugUnitTest', + ], + androidFolder.path), getTargetCheckCall(pluginExampleDirectory, 'ios'), getRunTestCall(pluginExampleDirectory, 'ios', destination: 'foo_destination'), diff --git a/script/tool/test/podspec_check_command_test.dart b/script/tool/test/podspec_check_command_test.dart index a1244a2337bc..a58154282731 100644 --- a/script/tool/test/podspec_check_command_test.dart +++ b/script/tool/test/podspec_check_command_test.dart @@ -143,6 +143,7 @@ void main() { .path, '--configuration=Debug', '--skip-tests', + '--allow-warnings', '--use-modular-headers', '--use-libraries' ], @@ -158,6 +159,7 @@ void main() { .path, '--configuration=Debug', '--skip-tests', + '--allow-warnings', '--use-modular-headers', ], packagesDir.path), diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index f2120040fc6d..b85e56064c9c 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -283,7 +283,9 @@ void main() { 'Test for publish-check command.', ); runner.addCommand(PublishCheckCommand(packagesDir, - processRunner: processRunner, httpClient: mockClient)); + platform: mockPlatform, + processRunner: processRunner, + httpClient: mockClient)); processRunner.mockProcessesForExecutable['flutter'] = [ FakeProcessInfo(MockProcess(exitCode: 1, stdout: 'Some error from pub'), @@ -339,7 +341,9 @@ void main() { 'Test for publish-check command.', ); runner.addCommand(PublishCheckCommand(packagesDir, - processRunner: processRunner, httpClient: mockClient)); + platform: mockPlatform, + processRunner: processRunner, + httpClient: mockClient)); final List output = await runCapturingPrint(runner, ['publish-check']); diff --git a/script/tool/test/publish_command_test.dart b/script/tool/test/publish_command_test.dart index 94023032ecb6..c8c14a6eaaf1 100644 --- a/script/tool/test/publish_command_test.dart +++ b/script/tool/test/publish_command_test.dart @@ -14,7 +14,6 @@ import 'package:flutter_plugin_tools/src/publish_command.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; import 'package:test/test.dart'; import 'common/package_command_test.mocks.dart'; @@ -22,11 +21,11 @@ import 'mocks.dart'; import 'util.dart'; void main() { - final String flutterCommand = getFlutterCommand(const LocalPlatform()); - + late MockPlatform platform; late Directory packagesDir; late MockGitDir gitDir; late TestProcessRunner processRunner; + late PublishCommand command; late CommandRunner commandRunner; late MockStdin mockStdin; late FileSystem fileSystem; @@ -34,13 +33,14 @@ void main() { late Map> mockHttpResponses; void createMockCredentialFile() { - final String credentialPath = PublishCommand.getCredentialPath(); - fileSystem.file(credentialPath) + fileSystem.file(command.credentialsPath) ..createSync(recursive: true) ..writeAsStringSync('some credential'); } setUp(() async { + platform = MockPlatform(isLinux: true); + platform.environment['HOME'] = '/home'; fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = TestProcessRunner(); @@ -71,14 +71,15 @@ void main() { }); mockStdin = MockStdin(); - commandRunner = CommandRunner('tester', '') - ..addCommand(PublishCommand( - packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - gitDir: gitDir, - httpClient: mockClient, - )); + command = PublishCommand( + packagesDir, + platform: platform, + processRunner: processRunner, + stdinput: mockStdin, + gitDir: gitDir, + httpClient: mockClient, + ); + commandRunner = CommandRunner('tester', '')..addCommand(command); }); group('Initial validation', () { @@ -145,8 +146,7 @@ void main() { createFakePlugin('plugin1', packagesDir, examples: []); createFakePlugin('plugin2', packagesDir, examples: []); - processRunner.mockProcessesForExecutable[flutterCommand] = - [ + processRunner.mockProcessesForExecutable['flutter'] = [ FakeProcessInfo( MockProcess( stdout: 'Foo', @@ -202,7 +202,7 @@ void main() { expect( processRunner.recordedCalls, contains(ProcessCall( - flutterCommand, + 'flutter', const ['pub', 'publish', '--dry-run', '--server=bar'], plugin.path))); }); @@ -225,7 +225,7 @@ void main() { expect( processRunner.recordedCalls, contains(ProcessCall( - flutterCommand, + 'flutter', const ['pub', 'publish', '--server=bar', '--force'], plugin.path))); }); @@ -249,21 +249,39 @@ void main() { processRunner.recordedCalls, containsAllInOrder([ ProcessCall( - flutterCommand, + 'flutter', const ['pub', 'publish', '--server=bar', '--force'], plugin1.path), ProcessCall( - flutterCommand, + 'flutter', const ['pub', 'publish', '--server=bar', '--force'], plugin2.path), ])); }); + test('creates credential file from envirnoment variable if necessary', + () async { + createFakePlugin('foo', packagesDir, examples: []); + const String credentials = 'some credential'; + platform.environment['PUB_CREDENTIALS'] = credentials; + + await runCapturingPrint(commandRunner, [ + 'publish', + '--packages=foo', + '--skip-confirmation', + '--pub-publish-flags', + '--server=bar' + ]); + + final File credentialFile = fileSystem.file(command.credentialsPath); + expect(credentialFile.existsSync(), true); + expect(credentialFile.readAsStringSync(), credentials); + }); + test('throws if pub publish fails', () async { createFakePlugin('foo', packagesDir, examples: []); - processRunner.mockProcessesForExecutable[flutterCommand] = - [ + processRunner.mockProcessesForExecutable['flutter'] = [ FakeProcessInfo(MockProcess(exitCode: 128), ['pub', 'publish']) ]; @@ -348,8 +366,7 @@ void main() { test('only if publishing succeeded', () async { createFakePlugin('foo', packagesDir, examples: []); - processRunner.mockProcessesForExecutable[flutterCommand] = - [ + processRunner.mockProcessesForExecutable['flutter'] = [ FakeProcessInfo(MockProcess(exitCode: 128), ['pub', 'publish']), ]; @@ -880,6 +897,44 @@ void main() { isNot(contains('git-push'))); }); }); + + group('credential location', () { + test('Linux with XDG', () async { + platform = MockPlatform(isLinux: true); + platform.environment['XDG_CONFIG_HOME'] = '/xdghome/config'; + command = PublishCommand(packagesDir, platform: platform); + + expect( + command.credentialsPath, '/xdghome/config/dart/pub-credentials.json'); + }); + + test('Linux without XDG', () async { + platform = MockPlatform(isLinux: true); + platform.environment['HOME'] = '/home'; + command = PublishCommand(packagesDir, platform: platform); + + expect( + command.credentialsPath, '/home/.config/dart/pub-credentials.json'); + }); + + test('macOS', () async { + platform = MockPlatform(isMacOS: true); + platform.environment['HOME'] = '/Users/someuser'; + command = PublishCommand(packagesDir, platform: platform); + + expect(command.credentialsPath, + '/Users/someuser/Library/Application Support/dart/pub-credentials.json'); + }); + + test('Windows', () async { + platform = MockPlatform(isWindows: true); + platform.environment['APPDATA'] = r'C:\Users\SomeUser\AppData'; + command = PublishCommand(packagesDir, platform: platform); + + expect(command.credentialsPath, + r'C:\Users\SomeUser\AppData\dart\pub-credentials.json'); + }); + }); } /// An extension of [RecordingProcessRunner] that stores 'flutter pub publish' @@ -893,7 +948,7 @@ class TestProcessRunner extends RecordingProcessRunner { {Directory? workingDirectory}) async { final io.Process process = await super.start(executable, args, workingDirectory: workingDirectory); - if (executable == getFlutterCommand(const LocalPlatform()) && + if (executable == 'flutter' && args.isNotEmpty && args[0] == 'pub' && args[1] == 'publish') { diff --git a/script/tool/test/update_min_sdk_command_test.dart b/script/tool/test/update_min_sdk_command_test.dart index b636c4af0756..91387489f350 100644 --- a/script/tool/test/update_min_sdk_command_test.dart +++ b/script/tool/test/update_min_sdk_command_test.dart @@ -91,6 +91,27 @@ void main() { expect(flutterVersion, '>=3.3.0'); }); + test('handles Flutter 3.10.0', () async { + final RepositoryPackage package = createFakePackage( + 'a_package', packagesDir, + isFlutter: true, + dartConstraint: '>=2.12.0 <4.0.0', + flutterConstraint: '>=2.10.0'); + + await runCapturingPrint(runner, [ + 'update-min-sdk', + '--flutter-min', + '3.10.0', // Corresponds to Dart 3.0.0 + ]); + + final String dartVersion = + package.parsePubspec().environment?['sdk'].toString() ?? ''; + final String flutterVersion = + package.parsePubspec().environment?['flutter'].toString() ?? ''; + expect(dartVersion, '>=3.0.0 <4.0.0'); + expect(flutterVersion, '>=3.10.0'); + }); + test('does not update Flutter if it is already higher', () async { final RepositoryPackage package = createFakePackage( 'a_package', packagesDir, diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index b614d8cef571..f3e6f9227dc8 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -874,6 +874,7 @@ tool/plugin/lib/plugin.dart processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: ''' +packages/plugin/example/android/.pluginToolsConfig.yaml packages/plugin/example/android/lint-baseline.xml packages/plugin/example/android/src/androidTest/foo/bar/FooTest.java packages/plugin/example/ios/RunnerTests/Foo.m diff --git a/third_party/packages/cupertino_icons/CHANGELOG.md b/third_party/packages/cupertino_icons/CHANGELOG.md index 982384b52ad5..2d791cf58dfd 100644 --- a/third_party/packages/cupertino_icons/CHANGELOG.md +++ b/third_party/packages/cupertino_icons/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. * Aligns Dart and Flutter SDK constraints. * Aligns Dart and Flutter SDK constraints. * Updates minimum SDK version to Flutter 3.0. diff --git a/third_party/packages/cupertino_icons/pubspec.yaml b/third_party/packages/cupertino_icons/pubspec.yaml index 89b8e5460f93..3cc91f09b9d1 100644 --- a/third_party/packages/cupertino_icons/pubspec.yaml +++ b/third_party/packages/cupertino_icons/pubspec.yaml @@ -6,7 +6,7 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 1.0.5 environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=2.18.0 <4.0.0" flutter: fonts: