Skip to content

Commit

Permalink
[ci] Switch Android unit tests to LUCI (flutter#4406)
Browse files Browse the repository at this point in the history
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 flutter/flutter#114373
  • Loading branch information
stuartmorgan authored Jul 15, 2023
1 parent 86c2b7d commit 166e2c2
Show file tree
Hide file tree
Showing 39 changed files with 343 additions and 221 deletions.
12 changes: 11 additions & 1 deletion .ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 3 additions & 5 deletions .ci/targets/android_platform_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions packages/camera/camera_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.10.8+4

* Adjusts SDK checks for better testability.

## 0.10.8+3

* Fixes unawaited_futures violations.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
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;
Expand Down Expand Up @@ -259,7 +257,7 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException {
// 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);
Expand Down Expand Up @@ -469,7 +467,7 @@ public void onClosed(@NonNull CameraCaptureSession session) {
};

// 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));
Expand Down Expand Up @@ -821,7 +819,7 @@ public void pauseVideoRecording(@NonNull final Result result) {
}

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);
Expand All @@ -842,7 +840,7 @@ public void resumeVideoRecording(@NonNull final Result result) {
}

try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (SdkCapabilityChecker.supportsVideoPause()) {
mediaRecorder.resume();
} else {
result.error(
Expand Down Expand Up @@ -1298,8 +1296,8 @@ public void setDescriptionWhileRecording(
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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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;

Expand All @@ -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,
Expand Down Expand Up @@ -69,8 +77,8 @@ public void setCameraState(@NonNull CameraState state) {
}

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ void configureOpenGL() {
"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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

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;

/**
Expand Down Expand Up @@ -55,7 +55,9 @@ public FpsRangeFeature(@NonNull CameraProperties cameraProperties) {
}

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@

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;

Expand All @@ -36,7 +35,7 @@ public NoiseReductionFeature(@NonNull CameraProperties cameraProperties) {
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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;

Expand Down Expand Up @@ -126,7 +127,7 @@ static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset)
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();
Expand Down Expand Up @@ -268,7 +269,7 @@ private void configureResolution(ResolutionPreset resolutionPreset, int cameraId
}
boolean captureSizeCalculated = false;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (SdkCapabilityChecker.supportsEncoderProfiles()) {
recordingProfileLegacy = null;
recordingProfile =
getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
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. */
Expand Down Expand Up @@ -37,7 +37,7 @@ public ZoomLevelFeature(@NonNull CameraProperties cameraProperties) {
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 {
Expand Down Expand Up @@ -83,7 +83,7 @@ public void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder) {
// 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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
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 {
Expand Down Expand Up @@ -78,7 +78,7 @@ public MediaRecorder build() throws IOException, NullPointerException, IndexOutO
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);

Expand Down
Loading

0 comments on commit 166e2c2

Please sign in to comment.