[ci] Switch Android unit tests to LUCI (#4406)

This moves Android unit tests from Cirrus to LUCI. In order to accomplish this:
- Switches the Android LUCI bots from JDK 11 to JDK 12, to resolve a crash when compiling `camera_android` unit tests with 11.
- Adds wrappers to SDK checks where necessary for testability, since the hack to override `Build.VERSION.SDK_INT` in unit tests (which was already giving warnings when run with JDK 11) no longer works at all in JDK 12.

Part of https://github.com/flutter/flutter/issues/114373
This commit is contained in:
stuartmorgan
2023-07-15 07:06:33 -04:00
committed by GitHub
parent 86c2b7da7b
commit 166e2c2709
39 changed files with 343 additions and 221 deletions

View File

@ -26,7 +26,7 @@ platform_properties:
dependencies: >-
[
{"dependency": "android_sdk", "version": "version:33v6"},
{"dependency": "open_jdk", "version": "version:11"},
{"dependency": "open_jdk", "version": "version:17"},
{"dependency": "curl", "version": "version:7.64.0"}
]
linux_desktop:
@ -286,6 +286,11 @@ targets:
version_file: flutter_master.version
target_file: android_build_all_packages.yaml
channel: master
# The legacy project build requires an older JDK.
dependencies: >-
[
{"dependency": "open_jdk", "version": "version:11"}
]
- name: Linux_android android_build_all_packages stable
recipe: packages/packages
@ -295,6 +300,11 @@ targets:
version_file: flutter_stable.version
target_file: android_build_all_packages.yaml
channel: stable
# The legacy project build requires an older JDK.
dependencies: >-
[
{"dependency": "open_jdk", "version": "version:11"}
]
- name: Linux_android android_platform_tests_shard_1 master
recipe: packages/packages

View File

@ -16,11 +16,9 @@ tasks:
# different exclusions.
# TODO(stuartmorgan): Eliminate the native unit test exclusion, and combine
# these steps.
# TODO(stuartmorgan): Enable this once https://github.com/flutter/flutter/issues/130148
# is resolved.
#- name: native unit tests
# script: script/tool_runner.sh
# args: ["native-test", "--android", "--no-integration", "--exclude=script/configs/exclude_native_unit_android.yaml"]
- name: native unit tests
script: script/tool_runner.sh
args: ["native-test", "--android", "--no-integration", "--exclude=script/configs/exclude_native_unit_android.yaml"]
# TODO(stuartmorgan): Enable these once
# https://github.com/flutter/flutter/issues/120736 is implemented.
# See also https://github.com/flutter/flutter/issues/114373

View File

@ -116,10 +116,6 @@ task:
CHANNEL: "stable"
MAPS_API_KEY: ENCRYPTED[d6583b08f79f91ea4844c77460f04539965e46ad2fd97fb7c062b4dfe88016228b86ebe8c220ab4187e0c4bd773dc1e7]
GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[1a2eebf9367197bbe812d9a0ea83a53a05aeba4bb5e4964fe6a69727883cd87e51238d39237b1f80b0894c48419ac268]
native_unit_test_script:
# Native integration tests are handled by Firebase Test Lab below, so
# only run unit tests.
- ./script/tool_runner.sh native-test --android --no-integration --exclude script/configs/exclude_native_unit_android.yaml
firebase_test_lab_script:
- if [[ -n "$GCLOUD_FIREBASE_TESTLAB_KEY" ]]; then
- echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json

View File

@ -1,3 +1,7 @@
## 0.10.8+4
* Adjusts SDK checks for better testability.
## 0.10.8+3
* Fixes unawaited_futures violations.

View File

@ -24,8 +24,6 @@ import android.media.EncoderProfiles;
import android.media.Image;
import android.media.ImageReader;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Handler;
import android.os.HandlerThread;
@ -259,7 +257,7 @@ class Camera
// TODO(camsim99): Revert changes that allow legacy code to be used when recordingProfile is null
// once this has largely been fixed on the Android side. https://github.com/flutter/flutter/issues/119668
EncoderProfiles recordingProfile = getRecordingProfile();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && recordingProfile != null) {
if (SdkCapabilityChecker.supportsEncoderProfiles() && recordingProfile != null) {
mediaRecorderBuilder = new MediaRecorderBuilder(recordingProfile, outputFilePath);
} else {
mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfileLegacy(), outputFilePath);
@ -469,7 +467,7 @@ class Camera
};
// Start the session.
if (VERSION.SDK_INT >= VERSION_CODES.P) {
if (SdkCapabilityChecker.supportsSessionConfiguration()) {
// Collect all surfaces to render to.
List<OutputConfiguration> configs = new ArrayList<>();
configs.add(new OutputConfiguration(flutterSurface));
@ -821,7 +819,7 @@ class Camera
}
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (SdkCapabilityChecker.supportsVideoPause()) {
mediaRecorder.pause();
} else {
result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null);
@ -842,7 +840,7 @@ class Camera
}
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (SdkCapabilityChecker.supportsVideoPause()) {
mediaRecorder.resume();
} else {
result.error(
@ -1298,8 +1296,8 @@ class Camera
return;
}
// See VideoRenderer.java requires API 26 to switch camera while recording
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {
// See VideoRenderer.java; support for this EGL extension is required to switch camera while recording.
if (!SdkCapabilityChecker.supportsEglRecordableAndroid()) {
result.error(
"setDescriptionWhileRecordingFailed",
"Device does not support switching the camera while recording",

View File

@ -11,6 +11,7 @@ import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import io.flutter.plugins.camera.types.CameraCaptureProperties;
import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper;
@ -25,6 +26,13 @@ class CameraCaptureCallback extends CaptureCallback {
private final CaptureTimeoutsWrapper captureTimeouts;
private final CameraCaptureProperties captureProps;
// Lookup keys for state; overrideable for unit tests since Mockito can't mock them.
@VisibleForTesting @NonNull
CaptureResult.Key<Integer> aeStateKey = CaptureResult.CONTROL_AE_STATE;
@VisibleForTesting @NonNull
CaptureResult.Key<Integer> afStateKey = CaptureResult.CONTROL_AE_STATE;
private CameraCaptureCallback(
@NonNull CameraCaptureStateListener cameraStateListener,
@NonNull CaptureTimeoutsWrapper captureTimeouts,
@ -69,8 +77,8 @@ class CameraCaptureCallback extends CaptureCallback {
}
private void process(CaptureResult result) {
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
Integer aeState = result.get(aeStateKey);
Integer afState = result.get(afStateKey);
// Update capture properties
if (result instanceof TotalCaptureResult) {

View File

@ -32,7 +32,7 @@ public final class CameraRegionUtils {
@NonNull
public static Size getCameraBoundaries(
@NonNull CameraProperties cameraProperties, @NonNull CaptureRequest.Builder requestBuilder) {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
if (SdkCapabilityChecker.supportsDistortionCorrection()
&& supportsDistortionCorrection(cameraProperties)) {
// Get the current distortion correction mode.
Integer distortionCorrectionMode =

View File

@ -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.camera;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
/** Wraps BUILD device info, allowing for overriding it in unit tests. */
public class DeviceInfo {
@VisibleForTesting public static @Nullable String BRAND = Build.BRAND;
@VisibleForTesting public static @Nullable String MODEL = Build.MODEL;
public static @Nullable String getBrand() {
return BRAND;
}
public static @Nullable String getModel() {
return MODEL;
}
}

View File

@ -0,0 +1,60 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugins.camera;
import android.annotation.SuppressLint;
import android.os.Build;
import androidx.annotation.ChecksSdkIntAtLeast;
import androidx.annotation.VisibleForTesting;
/** Abstracts SDK version checks, and allows overriding them in unit tests. */
public class SdkCapabilityChecker {
/** The current SDK version, overridable for testing. */
@SuppressLint("AnnotateVersionCheck")
@VisibleForTesting
public static int SDK_VERSION = Build.VERSION.SDK_INT;
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.P)
public static boolean supportsDistortionCorrection() {
// See https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#DISTORTION_CORRECTION_AVAILABLE_MODES
return SDK_VERSION >= Build.VERSION_CODES.P;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
public static boolean supportsEglRecordableAndroid() {
// See https://developer.android.com/reference/android/opengl/EGLExt#EGL_RECORDABLE_ANDROID
return SDK_VERSION >= Build.VERSION_CODES.O;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
public static boolean supportsEncoderProfiles() {
// See https://developer.android.com/reference/android/media/EncoderProfiles
return SDK_VERSION >= Build.VERSION_CODES.S;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.M)
public static boolean supportsMarshmallowNoiseReductionModes() {
// See https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES
return SDK_VERSION >= Build.VERSION_CODES.M;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.P)
public static boolean supportsSessionConfiguration() {
// See https://developer.android.com/reference/android/hardware/camera2/params/SessionConfiguration
return SDK_VERSION >= Build.VERSION_CODES.P;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.N)
public static boolean supportsVideoPause() {
// See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Pause
return SDK_VERSION >= Build.VERSION_CODES.N;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R)
public static boolean supportsZoomRatio() {
// See https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#CONTROL_ZOOM_RATIO
return SDK_VERSION >= Build.VERSION_CODES.R;
}
}

View File

@ -167,7 +167,7 @@ public class VideoRenderer {
"cannot configure OpenGL. missing EGL_ANDROID_presentation_time");
int[] attribList;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
if (SdkCapabilityChecker.supportsEglRecordableAndroid()) {
attribList =
new int[] {
EGL14.EGL_RED_SIZE, 8,

View File

@ -6,11 +6,11 @@ package io.flutter.plugins.camera.features.fpsrange;
import android.annotation.SuppressLint;
import android.hardware.camera2.CaptureRequest;
import android.os.Build;
import android.util.Range;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.plugins.camera.CameraProperties;
import io.flutter.plugins.camera.DeviceInfo;
import io.flutter.plugins.camera.features.CameraFeature;
/**
@ -55,7 +55,9 @@ public class FpsRangeFeature extends CameraFeature<Range<Integer>> {
}
private boolean isPixel4A() {
return Build.BRAND.equals("google") && Build.MODEL.equals("Pixel 4a");
String brand = DeviceInfo.getBrand();
String model = DeviceInfo.getModel();
return brand != null && brand.equals("google") && model != null && model.equals("Pixel 4a");
}
@NonNull

View File

@ -6,12 +6,11 @@ package io.flutter.plugins.camera.features.noisereduction;
import android.annotation.SuppressLint;
import android.hardware.camera2.CaptureRequest;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.Log;
import androidx.annotation.NonNull;
import io.flutter.BuildConfig;
import io.flutter.plugins.camera.CameraProperties;
import io.flutter.plugins.camera.SdkCapabilityChecker;
import io.flutter.plugins.camera.features.CameraFeature;
import java.util.HashMap;
@ -36,7 +35,7 @@ public class NoiseReductionFeature extends CameraFeature<NoiseReductionMode> {
NOISE_REDUCTION_MODES.put(NoiseReductionMode.fast, CaptureRequest.NOISE_REDUCTION_MODE_FAST);
NOISE_REDUCTION_MODES.put(
NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY);
if (VERSION.SDK_INT >= VERSION_CODES.M) {
if (SdkCapabilityChecker.supportsMarshmallowNoiseReductionModes()) {
NOISE_REDUCTION_MODES.put(
NoiseReductionMode.minimal, CaptureRequest.NOISE_REDUCTION_MODE_MINIMAL);
NOISE_REDUCTION_MODES.put(

View File

@ -15,6 +15,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.plugins.camera.CameraProperties;
import io.flutter.plugins.camera.SdkCapabilityChecker;
import io.flutter.plugins.camera.features.CameraFeature;
import java.util.List;
@ -126,7 +127,7 @@ public class ResolutionFeature extends CameraFeature<ResolutionPreset> {
if (preset.ordinal() > ResolutionPreset.high.ordinal()) {
preset = ResolutionPreset.high;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (SdkCapabilityChecker.supportsEncoderProfiles()) {
EncoderProfiles profile =
getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset);
List<EncoderProfiles.VideoProfile> videoProfiles = profile.getVideoProfiles();
@ -268,7 +269,7 @@ public class ResolutionFeature extends CameraFeature<ResolutionPreset> {
}
boolean captureSizeCalculated = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (SdkCapabilityChecker.supportsEncoderProfiles()) {
recordingProfileLegacy = null;
recordingProfile =
getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset);

View File

@ -7,9 +7,9 @@ package io.flutter.plugins.camera.features.zoomlevel;
import android.annotation.SuppressLint;
import android.graphics.Rect;
import android.hardware.camera2.CaptureRequest;
import android.os.Build;
import androidx.annotation.NonNull;
import io.flutter.plugins.camera.CameraProperties;
import io.flutter.plugins.camera.SdkCapabilityChecker;
import io.flutter.plugins.camera.features.CameraFeature;
/** Controls the zoom configuration on the {@link android.hardware.camera2} API. */
@ -37,7 +37,7 @@ public class ZoomLevelFeature extends CameraFeature<Float> {
return;
}
// On Android 11+ CONTROL_ZOOM_RATIO_RANGE should be use to get the zoom ratio directly as minimum zoom does not have to be 1.0f.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (SdkCapabilityChecker.supportsZoomRatio()) {
minimumZoomLevel = cameraProperties.getScalerMinZoomRatio();
maximumZoomLevel = cameraProperties.getScalerMaxZoomRatio();
} else {
@ -83,7 +83,7 @@ public class ZoomLevelFeature extends CameraFeature<Float> {
// On Android 11+ CONTROL_ZOOM_RATIO can be set to a zoom ratio and the camera feed will compute
// how to zoom on its own accounting for multiple logical cameras.
// Prior the image cropping window must be calculated and set manually.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (SdkCapabilityChecker.supportsZoomRatio()) {
requestBuilder.set(
CaptureRequest.CONTROL_ZOOM_RATIO,
ZoomUtils.computeZoomRatio(currentSetting, minimumZoomLevel, maximumZoomLevel));

View File

@ -7,8 +7,8 @@ package io.flutter.plugins.camera.media;
import android.media.CamcorderProfile;
import android.media.EncoderProfiles;
import android.media.MediaRecorder;
import android.os.Build;
import androidx.annotation.NonNull;
import io.flutter.plugins.camera.SdkCapabilityChecker;
import java.io.IOException;
public class MediaRecorderBuilder {
@ -78,7 +78,7 @@ public class MediaRecorderBuilder {
if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && encoderProfiles != null) {
if (SdkCapabilityChecker.supportsEncoderProfiles() && encoderProfiles != null) {
EncoderProfiles.VideoProfile videoProfile = encoderProfiles.getVideoProfiles().get(0);
EncoderProfiles.AudioProfile audioProfile = encoderProfiles.getAudioProfiles().get(0);

View File

@ -20,7 +20,6 @@ import io.flutter.plugins.camera.CameraCaptureCallback.CameraCaptureStateListene
import io.flutter.plugins.camera.types.CameraCaptureProperties;
import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper;
import io.flutter.plugins.camera.types.Timeout;
import io.flutter.plugins.camera.utils.TestUtils;
import java.util.HashMap;
import java.util.Map;
import junit.framework.TestCase;
@ -89,17 +88,13 @@ public class CameraCaptureCallbackStatesTest extends TestCase {
when(mockCaptureTimeouts.getPreCaptureFocusing()).thenReturn(mockTimeout);
when(mockCaptureTimeouts.getPreCaptureMetering()).thenReturn(mockTimeout);
Key<Integer> mockAeStateKey = mock(Key.class);
Key<Integer> mockAfStateKey = mock(Key.class);
TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AE_STATE", mockAeStateKey);
TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AF_STATE", mockAfStateKey);
mockedStaticTimeout.when(() -> Timeout.create(1000)).thenReturn(mockTimeout);
cameraCaptureCallback =
CameraCaptureCallback.create(
mockCaptureStateListener, mockCaptureTimeouts, mockCaptureProps);
cameraCaptureCallback.aeStateKey = mock(Key.class);
cameraCaptureCallback.afStateKey = mock(Key.class);
}
@Override
@ -107,17 +102,14 @@ public class CameraCaptureCallbackStatesTest extends TestCase {
super.tearDown();
mockedStaticTimeout.close();
TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AE_STATE", null);
TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AF_STATE", null);
}
@Override
protected void runTest() throws Throwable {
when(mockPartialCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(afState);
when(mockPartialCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(aeState);
when(mockTotalCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(afState);
when(mockTotalCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(aeState);
when(mockPartialCaptureResult.get(cameraCaptureCallback.afStateKey)).thenReturn(afState);
when(mockPartialCaptureResult.get(cameraCaptureCallback.aeStateKey)).thenReturn(aeState);
when(mockTotalCaptureResult.get(cameraCaptureCallback.afStateKey)).thenReturn(afState);
when(mockTotalCaptureResult.get(cameraCaptureCallback.aeStateKey)).thenReturn(aeState);
cameraCaptureCallback.setCameraState(cameraState);
if (isTimedOut) {

View File

@ -15,7 +15,6 @@ import android.graphics.Rect;
import android.hardware.camera2.CaptureRequest;
import android.os.Build;
import android.util.Size;
import io.flutter.plugins.camera.utils.TestUtils;
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockedStatic;
@ -242,6 +241,6 @@ public class CameraRegionUtils_getCameraBoundariesTest {
}
private static void updateSdkVersion(int version) {
TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", version);
SdkCapabilityChecker.SDK_VERSION = version;
}
}

View File

@ -150,7 +150,7 @@ public class CameraTest {
@After
public void after() {
TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 0);
SdkCapabilityChecker.SDK_VERSION = 0;
mockHandlerThreadFactory.close();
mockHandlerFactory.close();
}
@ -540,7 +540,7 @@ public class CameraTest {
MediaRecorder mockMediaRecorder = mock(MediaRecorder.class);
TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder);
TestUtils.setPrivateField(camera, "recordingVideo", true);
TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24);
SdkCapabilityChecker.SDK_VERSION = 24;
camera.pauseVideoRecording(mockResult);
@ -552,7 +552,7 @@ public class CameraTest {
@Test
public void pauseVideoRecording_shouldSendVideoRecordingFailedErrorWhenVersionCodeSmallerThenN() {
TestUtils.setPrivateField(camera, "recordingVideo", true);
TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 23);
SdkCapabilityChecker.SDK_VERSION = 23;
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
camera.pauseVideoRecording(mockResult);
@ -568,7 +568,7 @@ public class CameraTest {
MediaRecorder mockMediaRecorder = mock(MediaRecorder.class);
TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder);
TestUtils.setPrivateField(camera, "recordingVideo", true);
TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24);
SdkCapabilityChecker.SDK_VERSION = 24;
IllegalStateException expectedException = new IllegalStateException("Test error message");
@ -599,7 +599,7 @@ public class CameraTest {
MediaRecorder mockMediaRecorder = mock(MediaRecorder.class);
TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder);
TestUtils.setPrivateField(camera, "recordingVideo", true);
TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24);
SdkCapabilityChecker.SDK_VERSION = 24;
camera.resumeVideoRecording(mockResult);
@ -609,27 +609,40 @@ public class CameraTest {
}
@Test
public void setDescriptionWhileRecording() {
public void setDescriptionWhileRecording_errorsWhenUnsupported() {
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
MediaRecorder mockMediaRecorder = mock(MediaRecorder.class);
VideoRenderer mockVideoRenderer = mock(VideoRenderer.class);
TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder);
TestUtils.setPrivateField(camera, "recordingVideo", true);
TestUtils.setPrivateField(camera, "videoRenderer", mockVideoRenderer);
SdkCapabilityChecker.SDK_VERSION = Build.VERSION_CODES.LOLLIPOP;
final CameraProperties newCameraProperties = mock(CameraProperties.class);
camera.setDescriptionWhileRecording(mockResult, newCameraProperties);
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {
verify(mockResult, times(1))
.error(
eq("setDescriptionWhileRecordingFailed"),
eq("Device does not support switching the camera while recording"),
eq(null));
} else {
verify(mockResult, times(1)).success(null);
verify(mockResult, never()).error(any(), any(), any());
}
verify(mockResult, times(1))
.error(
eq("setDescriptionWhileRecordingFailed"),
eq("Device does not support switching the camera while recording"),
eq(null));
}
@Test
public void setDescriptionWhileRecording_succeedsWhenSupported() {
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
MediaRecorder mockMediaRecorder = mock(MediaRecorder.class);
VideoRenderer mockVideoRenderer = mock(VideoRenderer.class);
TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder);
TestUtils.setPrivateField(camera, "recordingVideo", true);
TestUtils.setPrivateField(camera, "videoRenderer", mockVideoRenderer);
SdkCapabilityChecker.SDK_VERSION = Build.VERSION_CODES.O;
final CameraProperties newCameraProperties = mock(CameraProperties.class);
camera.setDescriptionWhileRecording(mockResult, newCameraProperties);
verify(mockResult, times(1)).success(null);
verify(mockResult, never()).error(any(), any(), any());
}
@Test
@ -767,7 +780,7 @@ public class CameraTest {
public void
resumeVideoRecording_shouldSendVideoRecordingFailedErrorWhenVersionCodeSmallerThanN() {
TestUtils.setPrivateField(camera, "recordingVideo", true);
TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 23);
SdkCapabilityChecker.SDK_VERSION = 23;
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
@ -784,7 +797,7 @@ public class CameraTest {
MediaRecorder mockMediaRecorder = mock(MediaRecorder.class);
TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder);
TestUtils.setPrivateField(camera, "recordingVideo", true);
TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24);
SdkCapabilityChecker.SDK_VERSION = 24;
IllegalStateException expectedException = new IllegalStateException("Test error message");

View File

@ -7,10 +7,9 @@ package io.flutter.plugins.camera.features.fpsrange;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import android.os.Build;
import android.util.Range;
import io.flutter.plugins.camera.CameraProperties;
import io.flutter.plugins.camera.utils.TestUtils;
import io.flutter.plugins.camera.DeviceInfo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@ -19,8 +18,8 @@ import org.robolectric.RobolectricTestRunner;
public class FpsRangeFeaturePixel4aTest {
@Test
public void ctor_shouldInitializeFpsRangeWith30WhenDeviceIsPixel4a() {
TestUtils.setFinalStatic(Build.class, "BRAND", "google");
TestUtils.setFinalStatic(Build.class, "MODEL", "Pixel 4a");
DeviceInfo.BRAND = "google";
DeviceInfo.MODEL = "Pixel 4a";
FpsRangeFeature fpsRangeFeature = new FpsRangeFeature(mock(CameraProperties.class));
Range<Integer> range = fpsRangeFeature.getValue();

View File

@ -13,10 +13,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.hardware.camera2.CaptureRequest;
import android.os.Build;
import android.util.Range;
import io.flutter.plugins.camera.CameraProperties;
import io.flutter.plugins.camera.utils.TestUtils;
import io.flutter.plugins.camera.DeviceInfo;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -24,14 +23,14 @@ import org.junit.Test;
public class FpsRangeFeatureTest {
@Before
public void before() {
TestUtils.setFinalStatic(Build.class, "BRAND", "Test Brand");
TestUtils.setFinalStatic(Build.class, "MODEL", "Test Model");
DeviceInfo.BRAND = "Test Brand";
DeviceInfo.MODEL = "Test Model";
}
@After
public void after() {
TestUtils.setFinalStatic(Build.class, "BRAND", null);
TestUtils.setFinalStatic(Build.class, "MODEL", null);
DeviceInfo.BRAND = null;
DeviceInfo.MODEL = null;
}
@Test

View File

@ -15,9 +15,8 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.hardware.camera2.CaptureRequest;
import android.os.Build.VERSION;
import io.flutter.plugins.camera.CameraProperties;
import io.flutter.plugins.camera.utils.TestUtils;
import io.flutter.plugins.camera.SdkCapabilityChecker;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -25,15 +24,15 @@ import org.junit.Test;
public class NoiseReductionFeatureTest {
@Before
public void before() {
// Make sure the VERSION.SDK_INT field returns 23, to allow using all available
// Make sure the SDK_VERSION field returns 23, to allow using all available
// noise reduction modes in tests.
TestUtils.setFinalStatic(VERSION.class, "SDK_INT", 23);
SdkCapabilityChecker.SDK_VERSION = 23;
}
@After
public void after() {
// Make sure we reset the VERSION.SDK_INT field to it's original value.
TestUtils.setFinalStatic(VERSION.class, "SDK_INT", 0);
// Make sure we reset the SDK_VERSION field to it's original value.
SdkCapabilityChecker.SDK_VERSION = 0;
}
@Test

View File

@ -20,8 +20,7 @@ import android.graphics.Rect;
import android.hardware.camera2.CaptureRequest;
import android.os.Build;
import io.flutter.plugins.camera.CameraProperties;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import io.flutter.plugins.camera.SdkCapabilityChecker;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -118,7 +117,7 @@ public class ZoomLevelFeatureTest {
public void getValue_shouldReturnNullIfNotSet() {
ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties);
assertEquals(1.0, (float) zoomLevelFeature.getValue(), 0);
assertEquals(1.0, zoomLevelFeature.getValue(), 0);
}
@Test
@ -127,7 +126,7 @@ public class ZoomLevelFeatureTest {
zoomLevelFeature.setValue(2.3f);
assertEquals(2.3f, (float) zoomLevelFeature.getValue(), 0);
assertEquals(2.3f, zoomLevelFeature.getValue(), 0);
}
@Test
@ -209,11 +208,6 @@ public class ZoomLevelFeatureTest {
}
static void setSdkVersion(int sdkVersion) throws Exception {
Field sdkInt = Build.VERSION.class.getField("SDK_INT");
sdkInt.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(sdkInt, sdkInt.getModifiers() & ~Modifier.FINAL);
sdkInt.set(null, sdkVersion);
SdkCapabilityChecker.SDK_VERSION = sdkVersion;
}
}

View File

@ -5,25 +5,9 @@
package io.flutter.plugins.camera.utils;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import org.junit.Assert;
public class TestUtils {
public static <T> void setFinalStatic(Class<T> classToModify, String fieldName, Object newValue) {
try {
Field field = classToModify.getField(fieldName);
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
} catch (Exception e) {
Assert.fail("Unable to mock static field: " + fieldName);
}
}
public static <T> void setPrivateField(T instance, String fieldName, Object newValue) {
try {
Field field = instance.getClass().getDeclaredField(fieldName);

View File

@ -3,7 +3,7 @@ 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+3
version: 0.10.8+4
environment:
sdk: ">=2.18.0 <4.0.0"

View File

@ -1,3 +1,7 @@
## 0.5.0+2
* Adjusts SDK checks for better testability.
## 0.5.0+1
* Bumps androidx.annotation:annotation from 1.5.0 to 1.6.0.

View File

@ -15,6 +15,7 @@ import android.provider.DocumentsContract;
import android.provider.OpenableColumns;
import android.util.Log;
import android.webkit.MimeTypeMap;
import androidx.annotation.ChecksSdkIntAtLeast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@ -38,7 +39,8 @@ public class FileSelectorApiImpl implements GeneratedFileSelectorApi.FileSelecto
// Request code for selecting a directory.
private static final int OPEN_DIR = 223;
private final NativeObjectFactory objectFactory;
private final @NonNull NativeObjectFactory objectFactory;
private final @NonNull AndroidSdkChecker sdkChecker;
@Nullable ActivityPluginBinding activityPluginBinding;
private abstract static class OnResultListener {
@ -60,16 +62,28 @@ public class FileSelectorApiImpl implements GeneratedFileSelectorApi.FileSelecto
}
}
// Interface for an injectable SDK version checker.
@VisibleForTesting
interface AndroidSdkChecker {
@ChecksSdkIntAtLeast(parameter = 0)
boolean sdkIsAtLeast(int version);
}
public FileSelectorApiImpl(@NonNull ActivityPluginBinding activityPluginBinding) {
this(activityPluginBinding, new NativeObjectFactory());
this(
activityPluginBinding,
new NativeObjectFactory(),
(int version) -> Build.VERSION.SDK_INT >= version);
}
@VisibleForTesting
FileSelectorApiImpl(
@NonNull ActivityPluginBinding activityPluginBinding,
@NonNull NativeObjectFactory objectFactory) {
@NonNull NativeObjectFactory objectFactory,
@NonNull AndroidSdkChecker sdkChecker) {
this.activityPluginBinding = activityPluginBinding;
this.objectFactory = objectFactory;
this.sdkChecker = sdkChecker;
}
@Override
@ -171,9 +185,11 @@ public class FileSelectorApiImpl implements GeneratedFileSelectorApi.FileSelecto
@Override
public void getDirectoryPath(
@Nullable String initialDirectory, @NonNull GeneratedFileSelectorApi.Result<String> result) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
throw new UnsupportedOperationException(
"Selecting a directory is only supported on versions >= 21");
if (!sdkChecker.sdkIsAtLeast(android.os.Build.VERSION_CODES.LOLLIPOP)) {
result.error(
new UnsupportedOperationException(
"Selecting a directory is only supported on versions >= 21"));
return;
}
final Intent intent = objectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT_TREE);

View File

@ -24,11 +24,8 @@ import io.flutter.plugin.common.PluginRegistry;
import java.io.DataInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.List;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@ -69,23 +66,6 @@ public class FileSelectorAndroidPluginTest {
when(mockResolver.openInputStream(uri)).thenReturn(mock(InputStream.class));
}
@SuppressWarnings("JavaReflectionMemberAccess")
private static <T> void setFinalStatic(
Class<T> classToModify, String fieldName, Object newValue) {
try {
Field field = classToModify.getField(fieldName);
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
} catch (Exception e) {
Assert.fail("Unable to mock static field: " + fieldName);
}
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Test
public void openFileReturnsSuccessfully() throws FileNotFoundException {
@ -100,7 +80,8 @@ public class FileSelectorAndroidPluginTest {
when(mockActivity.getContentResolver()).thenReturn(mockContentResolver);
when(mockActivityBinding.getActivity()).thenReturn(mockActivity);
final FileSelectorApiImpl fileSelectorApi =
new FileSelectorApiImpl(mockActivityBinding, mockObjectFactory);
new FileSelectorApiImpl(
mockActivityBinding, mockObjectFactory, (version) -> Build.VERSION.SDK_INT >= version);
final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class);
fileSelectorApi.openFile(
@ -152,7 +133,8 @@ public class FileSelectorAndroidPluginTest {
when(mockActivity.getContentResolver()).thenReturn(mockContentResolver);
when(mockActivityBinding.getActivity()).thenReturn(mockActivity);
final FileSelectorApiImpl fileSelectorApi =
new FileSelectorApiImpl(mockActivityBinding, mockObjectFactory);
new FileSelectorApiImpl(
mockActivityBinding, mockObjectFactory, (version) -> Build.VERSION.SDK_INT >= version);
final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class);
fileSelectorApi.openFiles(
@ -207,15 +189,16 @@ public class FileSelectorAndroidPluginTest {
@SuppressWarnings({"rawtypes", "unchecked"})
@Test
public void getDirectoryPathReturnsSuccessfully() {
setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.LOLLIPOP);
final Uri mockUri = mock(Uri.class);
when(mockUri.toString()).thenReturn("some/path/");
when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT_TREE)).thenReturn(mockIntent);
when(mockActivityBinding.getActivity()).thenReturn(mockActivity);
final FileSelectorApiImpl fileSelectorApi =
new FileSelectorApiImpl(mockActivityBinding, mockObjectFactory);
new FileSelectorApiImpl(
mockActivityBinding,
mockObjectFactory,
(version) -> Build.VERSION_CODES.LOLLIPOP >= version);
final GeneratedFileSelectorApi.Result mockResult = mock(GeneratedFileSelectorApi.Result.class);
fileSelectorApi.getDirectoryPath(null, mockResult);
@ -232,4 +215,20 @@ public class FileSelectorAndroidPluginTest {
verify(mockResult).success("some/path/");
}
@Test
public void getDirectoryPath_errorsForUnsupportedVersion() {
final FileSelectorApiImpl fileSelectorApi =
new FileSelectorApiImpl(
mockActivityBinding,
mockObjectFactory,
(version) -> Build.VERSION_CODES.KITKAT >= version);
@SuppressWarnings("unchecked")
final GeneratedFileSelectorApi.Result<String> mockResult =
mock(GeneratedFileSelectorApi.Result.class);
fileSelectorApi.getDirectoryPath(null, mockResult);
verify(mockResult).error(any());
}
}

View File

@ -2,7 +2,7 @@ name: file_selector_android
description: Android implementation of the file_selector package.
repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_android
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22
version: 0.5.0+1
version: 0.5.0+2
environment:
sdk: ">=2.18.0 <4.0.0"

View File

@ -1,3 +1,7 @@
## 1.0.7
* Adjusts SDK checks for better testability.
## 1.0.6
* Removes obsolete null checks on non-nullable values.

View File

@ -9,7 +9,9 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutManager;
import android.os.Build;
import androidx.annotation.ChecksSdkIntAtLeast;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
@ -24,6 +26,23 @@ public class QuickActionsPlugin implements FlutterPlugin, ActivityAware, NewInte
private MethodChannel channel;
private MethodCallHandlerImpl handler;
private Activity activity;
private final @NonNull AndroidSdkChecker sdkChecker;
// Interface for an injectable SDK version checker.
@VisibleForTesting
interface AndroidSdkChecker {
@ChecksSdkIntAtLeast(parameter = 0)
boolean sdkIsAtLeast(int version);
}
public QuickActionsPlugin() {
this((int version) -> Build.VERSION.SDK_INT >= version);
}
@VisibleForTesting
QuickActionsPlugin(@NonNull AndroidSdkChecker capabilityChecker) {
this.sdkChecker = capabilityChecker;
}
/**
* Plugin registration.
@ -74,7 +93,7 @@ public class QuickActionsPlugin implements FlutterPlugin, ActivityAware, NewInte
@Override
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) {
if (!sdkChecker.sdkIsAtLeast(Build.VERSION_CODES.N_MR1)) {
return false;
}
// Notify the Dart side if the launch intent has the intent extra relevant to quick actions.

View File

@ -16,7 +16,6 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutManager;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding;
@ -25,9 +24,7 @@ import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.StandardMethodCodec;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import org.junit.After;
import org.junit.Test;
public class QuickActionsTest {
@ -75,9 +72,9 @@ public class QuickActionsTest {
throws NoSuchFieldException, IllegalAccessException {
// Arrange
final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger();
final QuickActionsPlugin plugin = new QuickActionsPlugin();
final QuickActionsPlugin plugin =
new QuickActionsPlugin((version) -> SUPPORTED_BUILD >= version);
setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin);
setBuildVersion(SUPPORTED_BUILD);
Field handler = plugin.getClass().getDeclaredField("handler");
handler.setAccessible(true);
handler.set(plugin, mock(MethodCallHandlerImpl.class));
@ -102,13 +99,12 @@ public class QuickActionsTest {
}
@Test
public void onNewIntent_buildVersionUnsupported_doesNotInvokeMethod()
throws NoSuchFieldException, IllegalAccessException {
public void onNewIntent_buildVersionUnsupported_doesNotInvokeMethod() {
// Arrange
final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger();
final QuickActionsPlugin plugin = new QuickActionsPlugin();
final QuickActionsPlugin plugin =
new QuickActionsPlugin((version) -> UNSUPPORTED_BUILD >= version);
setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin);
setBuildVersion(UNSUPPORTED_BUILD);
final Intent mockIntent = createMockIntentWithQuickActionExtra();
// Act
@ -120,13 +116,12 @@ public class QuickActionsTest {
}
@Test
public void onNewIntent_buildVersionSupported_invokesLaunchMethod()
throws NoSuchFieldException, IllegalAccessException {
public void onNewIntent_buildVersionSupported_invokesLaunchMethod() {
// Arrange
final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger();
final QuickActionsPlugin plugin = new QuickActionsPlugin();
final QuickActionsPlugin plugin =
new QuickActionsPlugin((version) -> SUPPORTED_BUILD >= version);
setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin);
setBuildVersion(SUPPORTED_BUILD);
final Intent mockIntent = createMockIntentWithQuickActionExtra();
final Activity mockMainActivity = mock(Activity.class);
when(mockMainActivity.getIntent()).thenReturn(mockIntent);
@ -161,19 +156,4 @@ public class QuickActionsTest {
when(mockIntent.getStringExtra(EXTRA_ACTION)).thenReturn(QuickActionsTest.SHORTCUT_TYPE);
return mockIntent;
}
private void setBuildVersion(int buildVersion)
throws NoSuchFieldException, IllegalAccessException {
Field buildSdkField = Build.VERSION.class.getField("SDK_INT");
buildSdkField.setAccessible(true);
final Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(buildSdkField, buildSdkField.getModifiers() & ~Modifier.FINAL);
buildSdkField.set(null, buildVersion);
}
@After
public void tearDown() throws NoSuchFieldException, IllegalAccessException {
setBuildVersion(0);
}
}

View File

@ -2,7 +2,7 @@ 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.6
version: 1.0.7
environment:
sdk: ">=2.18.0 <4.0.0"

View File

@ -1,3 +1,7 @@
## 3.9.1
* Adjusts SDK checks for better testability.
## 3.9.0
* Adds support for `WebResouceError.url`.

View File

@ -6,6 +6,7 @@ package io.flutter.plugins.webviewflutter;
import android.os.Build;
import android.webkit.CookieManager;
import androidx.annotation.ChecksSdkIntAtLeast;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import io.flutter.plugin.common.BinaryMessenger;
@ -25,6 +26,14 @@ public class CookieManagerHostApiImpl implements CookieManagerHostApi {
private final InstanceManager instanceManager;
private final CookieManagerProxy proxy;
private final @NonNull AndroidSdkChecker sdkChecker;
// Interface for an injectable SDK version checker.
@VisibleForTesting
interface AndroidSdkChecker {
@ChecksSdkIntAtLeast(parameter = 0)
boolean sdkIsAtLeast(int version);
}
/** Proxy for constructors and static method of `CookieManager`. */
@VisibleForTesting
@ -47,20 +56,25 @@ public class CookieManagerHostApiImpl implements CookieManagerHostApi {
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(
@VisibleForTesting
CookieManagerHostApiImpl(
@NonNull BinaryMessenger binaryMessenger,
@NonNull InstanceManager instanceManager,
@NonNull CookieManagerProxy proxy) {
this(
binaryMessenger, instanceManager, proxy, (int version) -> Build.VERSION.SDK_INT >= version);
}
@VisibleForTesting
CookieManagerHostApiImpl(
@NonNull BinaryMessenger binaryMessenger,
@NonNull InstanceManager instanceManager,
@NonNull CookieManagerProxy proxy,
@NonNull AndroidSdkChecker sdkChecker) {
this.binaryMessenger = binaryMessenger;
this.instanceManager = instanceManager;
this.proxy = proxy;
this.sdkChecker = sdkChecker;
}
@Override
@ -76,7 +90,7 @@ public class CookieManagerHostApiImpl implements CookieManagerHostApi {
@Override
public void removeAllCookies(
@NonNull Long identifier, @NonNull GeneratedAndroidWebView.Result<Boolean> result) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (sdkChecker.sdkIsAtLeast(Build.VERSION_CODES.LOLLIPOP)) {
getCookieManagerInstance(identifier).removeAllCookies(result::success);
} else {
result.success(removeCookiesPreL(getCookieManagerInstance(identifier)));
@ -86,7 +100,7 @@ public class CookieManagerHostApiImpl implements CookieManagerHostApi {
@Override
public void setAcceptThirdPartyCookies(
@NonNull Long identifier, @NonNull Long webViewIdentifier, @NonNull Boolean accept) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (sdkChecker.sdkIsAtLeast(Build.VERSION_CODES.LOLLIPOP)) {
getCookieManagerInstance(identifier)
.setAcceptThirdPartyCookies(
Objects.requireNonNull(instanceManager.getInstance(webViewIdentifier)), accept);

View File

@ -13,6 +13,7 @@ import android.view.ViewParent;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.ChecksSdkIntAtLeast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@ -74,6 +75,15 @@ public class WebViewHostApiImpl implements WebViewHostApi {
private WebViewClient currentWebViewClient;
private WebChromeClientHostApiImpl.SecureWebChromeClient currentWebChromeClient;
private final @NonNull AndroidSdkChecker sdkChecker;
// Interface for an injectable SDK version checker.
@VisibleForTesting
interface AndroidSdkChecker {
@ChecksSdkIntAtLeast(parameter = 0)
boolean sdkIsAtLeast(int version);
}
/**
* Creates a {@link WebViewPlatformView}.
*
@ -83,10 +93,24 @@ public class WebViewHostApiImpl implements WebViewHostApi {
@NonNull Context context,
@NonNull BinaryMessenger binaryMessenger,
@NonNull InstanceManager instanceManager) {
this(
context,
binaryMessenger,
instanceManager,
(int version) -> Build.VERSION.SDK_INT >= version);
}
@VisibleForTesting
WebViewPlatformView(
@NonNull Context context,
@NonNull BinaryMessenger binaryMessenger,
@NonNull InstanceManager instanceManager,
@NonNull AndroidSdkChecker sdkChecker) {
super(context);
currentWebViewClient = new WebViewClient();
currentWebChromeClient = new WebChromeClientHostApiImpl.SecureWebChromeClient();
api = new WebViewFlutterApiImpl(binaryMessenger, instanceManager);
this.sdkChecker = sdkChecker;
setWebViewClient(currentWebViewClient);
setWebChromeClient(currentWebChromeClient);
@ -108,7 +132,7 @@ public class WebViewHostApiImpl implements WebViewHostApi {
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (sdkChecker.sdkIsAtLeast(Build.VERSION_CODES.O)) {
final FlutterView flutterView = tryFindFlutterView();
if (flutterView != null) {
flutterView.setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);

View File

@ -15,7 +15,6 @@ 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;
@ -75,13 +74,15 @@ public class CookieManagerTest {
@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);
new CookieManagerHostApiImpl(
mockBinaryMessenger,
instanceManager,
new CookieManagerHostApiImpl.CookieManagerProxy(),
(int version) -> version <= Build.VERSION_CODES.LOLLIPOP);
final Boolean[] successResult = new Boolean[1];
hostApi.removeAllCookies(
@ -108,8 +109,6 @@ public class CookieManagerTest {
@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);
@ -120,7 +119,11 @@ public class CookieManagerTest {
instanceManager.addDartCreatedInstance(mockCookieManager, instanceIdentifier);
final CookieManagerHostApiImpl hostApi =
new CookieManagerHostApiImpl(mockBinaryMessenger, instanceManager);
new CookieManagerHostApiImpl(
mockBinaryMessenger,
instanceManager,
new CookieManagerHostApiImpl.CookieManagerProxy(),
(int version) -> version <= Build.VERSION_CODES.LOLLIPOP);
hostApi.setAcceptThirdPartyCookies(instanceIdentifier, webViewIdentifier, accept);

View File

@ -26,7 +26,6 @@ import io.flutter.embedding.android.FlutterView;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewFlutterApi;
import io.flutter.plugins.webviewflutter.WebViewHostApiImpl.WebViewPlatformView;
import io.flutter.plugins.webviewflutter.utils.TestUtils;
import java.util.HashMap;
import java.util.Objects;
import org.junit.After;
@ -345,13 +344,16 @@ public class WebViewTest {
@Test
public void setImportantForAutofillForParentFlutterView() {
final WebViewPlatformView webView =
new WebViewPlatformView(mockContext, mockBinaryMessenger, testInstanceManager);
new WebViewPlatformView(
mockContext,
mockBinaryMessenger,
testInstanceManager,
(int version) -> version <= Build.VERSION_CODES.O);
final WebViewPlatformView webViewSpy = spy(webView);
final FlutterView mockFlutterView = mock(FlutterView.class);
when(webViewSpy.getParent()).thenReturn(mockFlutterView);
TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.O);
webViewSpy.onAttachedToWindow();
verify(mockFlutterView).setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_YES);

View File

@ -1,26 +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.utils;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import org.junit.Assert;
public class TestUtils {
public static <T> void setFinalStatic(Class<T> classToModify, String fieldName, Object newValue) {
try {
Field field = classToModify.getField(fieldName);
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
} catch (Exception e) {
Assert.fail("Unable to mock static field: " + fieldName);
}
}
}

View File

@ -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.9.0
version: 3.9.1
environment:
sdk: ">=2.18.0 <4.0.0"