Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[camerax] Use AspectRatioStrategy and ResolutionFilter to help automatic selection of expected resolution #6357

Merged
merged 13 commits into from
Mar 27, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ public void setUp(
binaryMessenger, new FocusMeteringResultHostApiImpl(instanceManager));
meteringPointHostApiImpl = new MeteringPointHostApiImpl(instanceManager);
GeneratedCameraXLibrary.MeteringPointHostApi.setup(binaryMessenger, meteringPointHostApiImpl);
GeneratedCameraXLibrary.ResolutionFilterHostApi.setup(
binaryMessenger, new ResolutionFilterHostApiImpl(instanceManager));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2507,6 +2507,7 @@ public interface ResolutionSelectorHostApi {
void create(
@NonNull Long identifier,
@Nullable Long resolutionStrategyIdentifier,
@Nullable Long resolutionSelectorIdentifier,
@Nullable Long aspectRatioStrategyIdentifier);

/** The codec used by ResolutionSelectorHostApi. */
Expand All @@ -2530,13 +2531,17 @@ static void setup(
ArrayList<Object> args = (ArrayList<Object>) message;
Number identifierArg = (Number) args.get(0);
Number resolutionStrategyIdentifierArg = (Number) args.get(1);
Number aspectRatioStrategyIdentifierArg = (Number) args.get(2);
Number resolutionSelectorIdentifierArg = (Number) args.get(2);
Number aspectRatioStrategyIdentifierArg = (Number) args.get(3);
try {
api.create(
(identifierArg == null) ? null : identifierArg.longValue(),
(resolutionStrategyIdentifierArg == null)
? null
: resolutionStrategyIdentifierArg.longValue(),
(resolutionSelectorIdentifierArg == null)
? null
: resolutionSelectorIdentifierArg.longValue(),
(aspectRatioStrategyIdentifierArg == null)
? null
: aspectRatioStrategyIdentifierArg.longValue());
Expand Down Expand Up @@ -4189,4 +4194,77 @@ public void error(Throwable error) {
}
}
}

private static class ResolutionFilterHostApiCodec extends StandardMessageCodec {
public static final ResolutionFilterHostApiCodec INSTANCE = new ResolutionFilterHostApiCodec();

private ResolutionFilterHostApiCodec() {}

@Override
protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
switch (type) {
case (byte) 128:
return ResolutionInfo.fromList((ArrayList<Object>) readValue(buffer));
default:
return super.readValueOfType(type, buffer);
}
}

@Override
protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
if (value instanceof ResolutionInfo) {
stream.write(128);
writeValue(stream, ((ResolutionInfo) value).toList());
} else {
super.writeValue(stream, value);
}
}
}

/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
public interface ResolutionFilterHostApi {

void createWithOnePreferredSize(
@NonNull Long identifier, @NonNull ResolutionInfo preferredResolution);

/** The codec used by ResolutionFilterHostApi. */
static @NonNull MessageCodec<Object> getCodec() {
return ResolutionFilterHostApiCodec.INSTANCE;
}
/**
* Sets up an instance of `ResolutionFilterHostApi` to handle messages through the
* `binaryMessenger`.
*/
static void setup(
@NonNull BinaryMessenger binaryMessenger, @Nullable ResolutionFilterHostApi api) {
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.ResolutionFilterHostApi.createWithOnePreferredSize",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
Number identifierArg = (Number) args.get(0);
ResolutionInfo preferredResolutionArg = (ResolutionInfo) args.get(1);
try {
api.createWithOnePreferredSize(
(identifierArg == null) ? null : identifierArg.longValue(),
preferredResolutionArg);
wrapped.add(0, null);
} catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.resolutionselector.ResolutionFilter;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionFilterHostApi;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo;
import java.util.List;

/**
* Host API implementation for {@link ResolutionFilter}.
*
* <p>This class handles 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 ResolutionFilterHostApiImpl implements ResolutionFilterHostApi {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I don't think this matters that much but just for my understanding) Isn't this more of a ResolutionFilterConstructor/FactoryHostApiImpl? Because we aren't wrapping the methods of the ResolutionFilter interface but rather of a class that defines and creates an instance of a ResolutionFilter?

Copy link
Contributor Author

@camsim99 camsim99 Mar 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooo yes I like this, thank you! I will rename the proxy class it uses to clarify that this calls into a factory of sorts.

private final InstanceManager instanceManager;
private final ResolutionFilterProxy proxy;

/** Proxy for constructor of {@link ResolutionFilter}. */
@VisibleForTesting
public static class ResolutionFilterProxy {
/**
* Creates an instance of {@link ResolutionFilter} that moves the {@code preferredSize} to the
* front of the list of supported resolutions such that it can be prioritized by CameraX.
camsim99 marked this conversation as resolved.
Show resolved Hide resolved
*/
@NonNull
public ResolutionFilter createWithOnePreferredSize(@NonNull Size preferredSize) {
return new ResolutionFilter() {
@Override
@NonNull
public List<Size> filter(@NonNull List<Size> supportedSizes, int rotationDegrees) {
int preferredSizeIndex = supportedSizes.indexOf(preferredSize);

if (preferredSizeIndex > -1) {
supportedSizes.remove(preferredSizeIndex);
supportedSizes.add(0, preferredSize);
}

return supportedSizes;
}
};
}
}

/**
* Constructs a {@link ResolutionFilterHostApiImpl}.
*
* @param instanceManager maintains instances stored to communicate with attached Dart objects
*/
public ResolutionFilterHostApiImpl(@NonNull InstanceManager instanceManager) {
this(instanceManager, new ResolutionFilterProxy());
}

/**
* Constructs a {@link ResolutionFilterHostApiImpl}.
*
* @param instanceManager maintains instances stored to communicate with attached Dart objects
* @param proxy proxy for constructor of {@link ResolutionFilter}
*/
@VisibleForTesting
ResolutionFilterHostApiImpl(
@NonNull InstanceManager instanceManager, @NonNull ResolutionFilterProxy proxy) {
this.instanceManager = instanceManager;
this.proxy = proxy;
}

/**
* Creates a {@link ResolutionFilter} that prioritizes the specified {@code preferredResolution}
* over all other resolutions.
*/
@Override
public void createWithOnePreferredSize(
@NonNull Long identifier, @NonNull ResolutionInfo preferredResolution) {
Size preferredSize =
new Size(
preferredResolution.getWidth().intValue(), preferredResolution.getHeight().intValue());
instanceManager.addDartCreatedInstance(
proxy.createWithOnePreferredSize(preferredSize), identifier);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.resolutionselector.AspectRatioStrategy;
import androidx.camera.core.resolutionselector.ResolutionFilter;
import androidx.camera.core.resolutionselector.ResolutionSelector;
import androidx.camera.core.resolutionselector.ResolutionStrategy;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionSelectorHostApi;
Expand All @@ -30,11 +31,15 @@ public static class ResolutionSelectorProxy {
@NonNull
public ResolutionSelector create(
@Nullable ResolutionStrategy resolutionStrategy,
@Nullable AspectRatioStrategy aspectRatioStrategy) {
@Nullable AspectRatioStrategy aspectRatioStrategy,
@Nullable ResolutionFilter resolutionFilter) {
final ResolutionSelector.Builder builder = new ResolutionSelector.Builder();
if (resolutionStrategy != null) {
builder.setResolutionStrategy(resolutionStrategy);
}
if (resolutionFilter != null) {
builder.setResolutionFilter(resolutionFilter);
}
if (aspectRatioStrategy != null) {
builder.setAspectRatioStrategy(aspectRatioStrategy);
}
Expand Down Expand Up @@ -65,13 +70,14 @@ public ResolutionSelectorHostApiImpl(@NonNull InstanceManager instanceManager) {
}

/**
* Creates a {@link ResolutionSelector} instance with the {@link ResolutionStrategy} and {@link
* AspectRatio} that have the identifiers specified if provided.
* Creates a {@link ResolutionSelector} instance with the {@link ResolutionStrategy}, {@link
* ResolutionFilter}, and {@link AspectRatio} that have the identifiers specified if provided.
*/
@Override
public void create(
@NonNull Long identifier,
@Nullable Long resolutionStrategyIdentifier,
@Nullable Long resolutionFilterIdentifier,
@Nullable Long aspectRatioStrategyIdentifier) {
instanceManager.addDartCreatedInstance(
proxy.create(
Expand All @@ -81,7 +87,10 @@ public void create(
aspectRatioStrategyIdentifier == null
? null
: Objects.requireNonNull(
instanceManager.getInstance(aspectRatioStrategyIdentifier))),
instanceManager.getInstance(aspectRatioStrategyIdentifier)),
resolutionFilterIdentifier == null
? null
: Objects.requireNonNull(instanceManager.getInstance(resolutionFilterIdentifier))),
identifier);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.plugins.camerax;

import static org.junit.Assert.assertEquals;

import android.util.Size;
import androidx.camera.core.resolutionselector.ResolutionFilter;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo;
import java.util.ArrayList;
import java.util.List;
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 ResolutionFilterTest {
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@Mock public ResolutionFilter mockResolutionFilter;

InstanceManager instanceManager;

@Before
public void setUp() {
instanceManager = InstanceManager.create(identifier -> {});
}

@After
public void tearDown() {
instanceManager.stopFinalizationListener();
}

@Test
public void hostApiCreateWithOnePreferredSize_createsExpectedResolutionFilterInstance() {
final android.util.Size lala = new android.util.Size(720, 480);
final ResolutionFilterHostApiImpl hostApi = new ResolutionFilterHostApiImpl(instanceManager);
final long instanceIdentifier = 50;
final long preferredResolutionWidth = 20;
final long preferredResolutionHeight = 80;
final ResolutionInfo preferredResolution =
new ResolutionInfo.Builder()
.setWidth(preferredResolutionWidth)
.setHeight(preferredResolutionHeight)
.build();

hostApi.createWithOnePreferredSize(instanceIdentifier, preferredResolution);

// Test instance filters supported resolutions as expected.
final ResolutionFilter resolutionFilter = instanceManager.getInstance(instanceIdentifier);
final Size fakeSupportedSize1 = new Size(720, 480);
final Size fakeSupportedSize2 = new Size(20, 80);
final Size fakeSupportedSize3 = new Size(2, 8);
final Size preferredSize =
new Size((int) preferredResolutionWidth, (int) preferredResolutionHeight);

final ArrayList<Size> fakeSupportedSizes = new ArrayList<Size>();
fakeSupportedSizes.add(fakeSupportedSize1);
fakeSupportedSizes.add(fakeSupportedSize2);
fakeSupportedSizes.add(preferredSize);
fakeSupportedSizes.add(fakeSupportedSize3);

// Test case where preferred resolution is supported.
List<Size> filteredSizes = resolutionFilter.filter(fakeSupportedSizes, 90);
assertEquals(filteredSizes.get(0), preferredSize);

// Test case where preferred resolution is not supported.
fakeSupportedSizes.remove(0);
filteredSizes = resolutionFilter.filter(fakeSupportedSizes, 90);
assertEquals(filteredSizes, fakeSupportedSizes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static org.mockito.Mockito.when;

import androidx.camera.core.resolutionselector.AspectRatioStrategy;
import androidx.camera.core.resolutionselector.ResolutionFilter;
import androidx.camera.core.resolutionselector.ResolutionSelector;
import androidx.camera.core.resolutionselector.ResolutionStrategy;
import org.junit.After;
Expand Down Expand Up @@ -42,17 +43,25 @@ public void hostApiCreate_createsExpectedResolutionSelectorInstance() {
final long resolutionStrategyIdentifier = 14;
instanceManager.addDartCreatedInstance(mockResolutionStrategy, resolutionStrategyIdentifier);

final ResolutionFilter mockResolutionFilter = mock(ResolutionFilter.class);
final long resolutionFilterIdentifier = 33;
instanceManager.addDartCreatedInstance(mockResolutionFilter, resolutionFilterIdentifier);

final AspectRatioStrategy mockAspectRatioStrategy = mock(AspectRatioStrategy.class);
final long aspectRatioStrategyIdentifier = 15;
instanceManager.addDartCreatedInstance(mockAspectRatioStrategy, aspectRatioStrategyIdentifier);

when(mockProxy.create(mockResolutionStrategy, mockAspectRatioStrategy))
when(mockProxy.create(mockResolutionStrategy, mockAspectRatioStrategy, mockResolutionFilter))
.thenReturn(mockResolutionSelector);
final ResolutionSelectorHostApiImpl hostApi =
new ResolutionSelectorHostApiImpl(instanceManager, mockProxy);

final long instanceIdentifier = 0;
hostApi.create(instanceIdentifier, resolutionStrategyIdentifier, aspectRatioStrategyIdentifier);
hostApi.create(
instanceIdentifier,
resolutionStrategyIdentifier,
resolutionFilterIdentifier,
aspectRatioStrategyIdentifier);

assertEquals(instanceManager.getInstance(instanceIdentifier), mockResolutionSelector);
}
Expand Down
Loading