Adapt Meter and metering package to picture use

pull/580/head
Mattia Iavarone 6 years ago
parent f7e9372caf
commit 5725276553
  1. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  2. 51
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Meter.java
  3. 44
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/metering/AutoExposure.java
  4. 46
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/metering/AutoFocus.java
  5. 39
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/metering/AutoWhiteBalance.java
  6. 53
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/metering/MeteringParameter.java

@ -83,6 +83,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
private CaptureRequest.Builder mRepeatingRequestBuilder;
private CaptureRequest mRepeatingRequest;
private CameraCaptureSession.CaptureCallback mRepeatingRequestCallback;
private CaptureResult mLastRepeatingResult;
private final Camera2Mapper mMapper = Camera2Mapper.get();
// Frame processing
@ -240,6 +241,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
mLastRepeatingResult = result;
if (mPictureRecorder instanceof Full2PictureRecorder) {
((Full2PictureRecorder) mPictureRecorder).onCaptureCompleted(result);
}
@ -1175,7 +1177,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
mRepeatingRequestBuilder,
mCameraCharacteristics,
Camera2Engine.this);
mMeter.startMetering(point, gesture);
mMeter.startMetering(mLastRepeatingResult, point, gesture);
}
});
}
@ -1187,9 +1189,11 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
* @param gesture gesture
*/
@Override
public void onMeteringStarted(@NonNull PointF point, @Nullable Gesture gesture) {
public void onMeteringStarted(@Nullable PointF point, @Nullable Gesture gesture) {
LOG.w("onMeteringStarted - point:", point, "gesture:", gesture);
if (point != null) {
mCallback.dispatchOnFocusStart(gesture, point);
}
applyRepeatingRequestBuilder();
}
@ -1201,10 +1205,12 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
* @param success success
*/
@Override
public void onMeteringEnd(@NonNull PointF point, @Nullable Gesture gesture, boolean success) {
public void onMeteringEnd(@Nullable PointF point, @Nullable Gesture gesture, boolean success) {
LOG.w("onMeteringEnd - point:", point, "gesture:", gesture, "success:", success);
if (point != null) {
mCallback.dispatchOnFocusEnd(gesture, success, point);
}
}
/**
* When metering is reset, we're not sure that the engine is still alive.
@ -1214,7 +1220,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
* @return true if metering can be reset
*/
@Override
public boolean canResetMetering(@NonNull PointF point, @Nullable Gesture gesture) {
public boolean canResetMetering(@Nullable PointF point, @Nullable Gesture gesture) {
return getEngineState() == STATE_STARTED;
}
@ -1225,7 +1231,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
* @param gesture gesture
*/
@Override
public void onMeteringReset(@NonNull PointF point, @Nullable Gesture gesture) {
public void onMeteringReset(@Nullable PointF point, @Nullable Gesture gesture) {
applyDefaultFocus(mRepeatingRequestBuilder);
applyRepeatingRequestBuilder(); // only if preview started already
}

@ -24,6 +24,7 @@ import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -52,7 +53,7 @@ public class Meter {
* @param point point
* @param gesture gesture
*/
void onMeteringStarted(@NonNull PointF point, @Nullable Gesture gesture);
void onMeteringStarted(@Nullable PointF point, @Nullable Gesture gesture);
/**
* Notifies that metering has ended. No action is required for implementors.
@ -62,16 +63,16 @@ public class Meter {
* @param gesture gesture
* @param success success
*/
void onMeteringEnd(@NonNull PointF point, @Nullable Gesture gesture, boolean success);
void onMeteringEnd(@Nullable PointF point, @Nullable Gesture gesture, boolean success);
/**
* Notifies that metering has been reset. From now on, this meter instance
* is done, although in theory it could be reused by calling
* {@link #startMetering(PointF, Gesture)} again.
* {@link #startMetering(CaptureResult, PointF, Gesture)} again.
* @param point point
* @param gesture gesture
*/
void onMeteringReset(@NonNull PointF point, @Nullable Gesture gesture);
void onMeteringReset(@Nullable PointF point, @Nullable Gesture gesture);
/**
* Whether metering can be reset. Since it happens at a future time, this should
@ -81,7 +82,7 @@ public class Meter {
* @return true if can reset
*/
// TODO is this useful? engine could do its checks onMeteringReset()
boolean canResetMetering(@NonNull PointF point, @Nullable Gesture gesture);
boolean canResetMetering(@Nullable PointF point, @Nullable Gesture gesture);
}
private static final String TAG = Meter.class.getSimpleName();
@ -121,15 +122,18 @@ public class Meter {
/**
* Starts a metering sequence.
* @param lastResult the last result
* @param point point
* @param gesture gesture
*/
@SuppressWarnings("WeakerAccess")
public void startMetering(@NonNull PointF point, @Nullable Gesture gesture) {
public void startMetering(@NonNull CaptureResult lastResult, @Nullable PointF point, @Nullable Gesture gesture) {
mPoint = point;
mGesture = gesture;
mIsMetering = true;
List<MeteringRectangle> areas = new ArrayList<>();
if (point != null) {
// This is a good Q/A. https://stackoverflow.com/a/33181620/4288782
// At first, the point is relative to the View system and does not account our own cropping.
// Will keep updating these two below.
@ -160,14 +164,19 @@ public class Meter {
// can be obtained through the SENSOR rotated preview stream size.
Size visibleSize = mEngine.getPreviewStreamSize(Reference.SENSOR);
//noinspection ConstantConditions
MeteringRectangle area1 = createMeteringRectangle(referenceSize, referencePoint, visibleSize, 0.05F, 1000);
MeteringRectangle area2 = createMeteringRectangle(referenceSize, referencePoint, visibleSize, 0.1F, 100);
List<MeteringRectangle> areas = Arrays.asList(area1, area2);
MeteringRectangle area1 = createMeteringRectangle(referenceSize, referencePoint,
visibleSize, 0.05F, 1000);
MeteringRectangle area2 = createMeteringRectangle(referenceSize, referencePoint,
visibleSize, 0.1F, 100);
areas.add(area1);
areas.add(area2);
}
// 7. And finally dispatch everything
mAutoFocus.startMetering(mCharacteristics, mBuilder, areas);
mAutoWhiteBalance.startMetering(mCharacteristics, mBuilder, areas);
mAutoExposure.startMetering(mCharacteristics, mBuilder, areas);
boolean skipIfPossible = mPoint == null;
mAutoFocus.startMetering(mCharacteristics, mBuilder, areas, lastResult, skipIfPossible);
mAutoWhiteBalance.startMetering(mCharacteristics, mBuilder, areas, lastResult, skipIfPossible);
mAutoExposure.startMetering(mCharacteristics, mBuilder, areas, lastResult, skipIfPossible);
// Dispatch to callback
mCallback.onMeteringStarted(mPoint, mGesture);
@ -310,7 +319,7 @@ public class Meter {
/**
* True if we're metering. False if we're not, for example if we're waiting for
* a reset call, or if {@link #startMetering(PointF, Gesture)} was never called.
* a reset call, or if {@link #startMetering(CaptureResult, PointF, Gesture)} was never called.
* @return true if metering
*/
@SuppressWarnings("WeakerAccess")
@ -361,12 +370,16 @@ public class Meter {
mEngine.mHandler.remove(mResetRunnable);
if (mCallback.canResetMetering(mPoint, mGesture)) {
LOG.i("Resetting the meter parameters.");
Rect whole = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
if (whole == null) whole = new Rect();
MeteringRectangle rectangle = new MeteringRectangle(whole, MeteringRectangle.METERING_WEIGHT_DONT_CARE);
mAutoFocus.resetMetering(mCharacteristics, mBuilder, rectangle);
mAutoWhiteBalance.resetMetering(mCharacteristics, mBuilder, rectangle);
mAutoExposure.resetMetering(mCharacteristics, mBuilder, rectangle);
MeteringRectangle whole = null;
if (mPoint != null) {
// If we have a point, we must reset the metering areas.
Rect wholeRect = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
if (wholeRect == null) wholeRect = new Rect();
whole = new MeteringRectangle(wholeRect, MeteringRectangle.METERING_WEIGHT_DONT_CARE);
}
mAutoFocus.resetMetering(mCharacteristics, mBuilder, whole);
mAutoWhiteBalance.resetMetering(mCharacteristics, mBuilder, whole);
mAutoExposure.resetMetering(mCharacteristics, mBuilder, whole);
mCallback.onMeteringReset(mPoint, mGesture);
}
}

@ -7,6 +7,7 @@ import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger;
@ -22,13 +23,9 @@ public class AutoExposure extends MeteringParameter {
private boolean isStarted;
@Override
public void startMetering(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder,
@NonNull List<MeteringRectangle> areas) {
isSuccessful = false;
isMetered = false;
isStarted = false;
protected boolean checkSupportsProcessing(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder) {
// In our case, this means checking if we support the AE precapture trigger.
boolean isNotLegacy = readCharacteristic(characteristics,
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) !=
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
@ -39,9 +36,23 @@ public class AutoExposure extends MeteringParameter {
|| aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH
|| aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE
|| aeMode == 5 /* CameraCharacteristics.CONTROL_AE_MODE_ON_EXTERNAL_FLASH, API 28 */);
isSupported = isNotLegacy && isAEOn;
return isNotLegacy && isAEOn;
}
@Override
protected boolean checkShouldSkip(@NonNull CaptureResult lastResult) {
Integer aeState = lastResult.get(CaptureResult.CONTROL_AE_STATE);
return aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED;
}
@Override
protected void onStartMetering(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder,
@NonNull List<MeteringRectangle> areas,
boolean supportsProcessing) {
isStarted = false;
if (isSupported) {
if (supportsProcessing) {
builder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
}
@ -49,16 +60,16 @@ public class AutoExposure extends MeteringParameter {
// Even if precapture is not supported, check the regions anyway.
int maxRegions = readCharacteristic(characteristics,
CameraCharacteristics.CONTROL_MAX_REGIONS_AE, 0);
if (maxRegions > 0) {
if (!areas.isEmpty() && maxRegions > 0) {
int max = Math.min(maxRegions, areas.size());
builder.set(CaptureRequest.CONTROL_AE_REGIONS,
areas.subList(0, max).toArray(new MeteringRectangle[]{}));
}
}
@Override
public void onCapture(@NonNull CaptureResult result) {
if (isMetered || !isSupported) return;
public void processCapture(@NonNull CaptureResult result) {
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
LOG.i("onCapture:", "aeState:", aeState);
if (aeState == null) return;
@ -82,15 +93,16 @@ public class AutoExposure extends MeteringParameter {
}
@Override
public void resetMetering(@NonNull CameraCharacteristics characteristics,
protected void onResetMetering(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder,
@NonNull MeteringRectangle area) {
@Nullable MeteringRectangle area,
boolean supportsProcessing) {
int maxRegions = readCharacteristic(characteristics,
CameraCharacteristics.CONTROL_MAX_REGIONS_AE, 0);
if (maxRegions > 0) {
if (area != null && maxRegions > 0) {
builder.set(CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[]{area});
}
if (isSupported) {
if (supportsProcessing) {
// Cleanup any precapture sequence.
if (Build.VERSION.SDK_INT >= 23) {
builder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,

@ -7,6 +7,7 @@ import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger;
@ -20,36 +21,44 @@ public class AutoFocus extends MeteringParameter {
private static final CameraLogger LOG = CameraLogger.create(TAG);
@Override
public void startMetering(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder,
@NonNull List<MeteringRectangle> areas) {
isSuccessful = false;
isMetered = false;
Integer afMode = builder.get(CaptureRequest.CONTROL_AF_MODE);
protected boolean checkSupportsProcessing(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder) {
// Exclude OFF and EDOF as per docs.
isSupported = afMode != null &&
Integer afMode = builder.get(CaptureRequest.CONTROL_AF_MODE);
return afMode != null &&
(afMode == CameraCharacteristics.CONTROL_AF_MODE_AUTO
|| afMode == CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE
|| afMode == CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_VIDEO
|| afMode == CameraCharacteristics.CONTROL_AF_MODE_MACRO);
if (isSupported) {
}
@Override
protected boolean checkShouldSkip(@NonNull CaptureResult lastResult) {
Integer afState = lastResult.get(CaptureResult.CONTROL_AF_STATE);
return afState != null && afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED;
}
@Override
protected void onStartMetering(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder,
@NonNull List<MeteringRectangle> areas,
boolean supportsProcessing) {
if (supportsProcessing) {
builder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START);
}
// Even if auto is not supported, change the regions anyway.
int maxRegions = readCharacteristic(characteristics, CameraCharacteristics.CONTROL_MAX_REGIONS_AF, 0);
if (maxRegions > 0) {
int maxRegions = readCharacteristic(characteristics,
CameraCharacteristics.CONTROL_MAX_REGIONS_AF, 0);
if (!areas.isEmpty() && maxRegions > 0) {
int max = Math.min(maxRegions, areas.size());
builder.set(CaptureRequest.CONTROL_AF_REGIONS,
areas.subList(0, max).toArray(new MeteringRectangle[]{}));
}
}
@Override
public void onCapture(@NonNull CaptureResult result) {
if (isMetered || !isSupported) return;
public void processCapture(@NonNull CaptureResult result) {
Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
LOG.i("onCapture:", "afState:", afState);
if (afState == null) return;
@ -71,16 +80,17 @@ public class AutoFocus extends MeteringParameter {
}
@Override
public void resetMetering(@NonNull CameraCharacteristics characteristics,
protected void onResetMetering(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder,
@NonNull MeteringRectangle area) {
@Nullable MeteringRectangle area,
boolean supportsProcessing) {
int maxRegions = readCharacteristic(characteristics,
CameraCharacteristics.CONTROL_MAX_REGIONS_AF, 0);
if (maxRegions > 0) {
if (area != null && maxRegions > 0) {
builder.set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{area});
}
if (isSupported) { // Cleanup any trigger.
if (supportsProcessing) { // Cleanup any trigger.
builder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
}
}

@ -7,6 +7,7 @@ import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger;
@ -20,37 +21,44 @@ public class AutoWhiteBalance extends MeteringParameter {
private static final CameraLogger LOG = CameraLogger.create(TAG);
@Override
public void startMetering(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder,
@NonNull List<MeteringRectangle> areas) {
isSuccessful = false;
isMetered = false;
protected boolean checkSupportsProcessing(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder) {
boolean isNotLegacy = readCharacteristic(characteristics,
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1) !=
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
Integer awbMode = builder.get(CaptureRequest.CONTROL_AWB_MODE);
isSupported = isNotLegacy && awbMode != null && awbMode == CaptureRequest.CONTROL_AWB_MODE_AUTO;
return isNotLegacy && awbMode != null && awbMode == CaptureRequest.CONTROL_AWB_MODE_AUTO;
}
if (isSupported) {
@Override
protected boolean checkShouldSkip(@NonNull CaptureResult lastResult) {
Integer awbState = lastResult.get(CaptureResult.CONTROL_AWB_STATE);
return awbState != null && awbState == CaptureRequest.CONTROL_AWB_STATE_CONVERGED;
}
@Override
protected void onStartMetering(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder,
@NonNull List<MeteringRectangle> areas,
boolean supportsProcessing) {
if (supportsProcessing) {
// Remove any lock. We're not setting any, but just in case.
// This would make processing be stuck into the process method.
builder.set(CaptureRequest.CONTROL_AWB_LOCK, false);
}
// Even if auto is not supported, change the regions anyway.
int maxRegions = readCharacteristic(characteristics,
CameraCharacteristics.CONTROL_MAX_REGIONS_AWB, 0);
if (maxRegions > 0) {
if (!areas.isEmpty() && maxRegions > 0) {
int max = Math.min(maxRegions, areas.size());
builder.set(CaptureRequest.CONTROL_AWB_REGIONS,
areas.subList(0, max).toArray(new MeteringRectangle[]{}));
}
}
@Override
public void onCapture(@NonNull CaptureResult result) {
if (isMetered || !isSupported) return;
public void processCapture(@NonNull CaptureResult result) {
Integer awbState = result.get(CaptureResult.CONTROL_AWB_STATE);
LOG.i("onCapture:", "awbState:", awbState);
if (awbState == null) return;
@ -69,12 +77,13 @@ public class AutoWhiteBalance extends MeteringParameter {
}
@Override
public void resetMetering(@NonNull CameraCharacteristics characteristics,
protected void onResetMetering(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder,
@NonNull MeteringRectangle area) {
@Nullable MeteringRectangle area,
boolean supportsProcessing) {
int maxRegions = readCharacteristic(characteristics,
CameraCharacteristics.CONTROL_MAX_REGIONS_AWB, 0);
if (maxRegions > 0) {
if (area != null && maxRegions > 0) {
builder.set(CaptureRequest.CONTROL_AWB_REGIONS, new MeteringRectangle[]{area});
}
}

@ -7,6 +7,7 @@ import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.util.List;
@ -14,15 +15,15 @@ import java.util.List;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class MeteringParameter {
@SuppressWarnings("WeakerAccess")
protected boolean isSupported;
@SuppressWarnings("WeakerAccess")
protected boolean isSuccessful;
@SuppressWarnings("WeakerAccess")
protected boolean isMetered;
private boolean shouldSkip;
private boolean supportsProcessing;
@SuppressWarnings("WeakerAccess")
@NonNull
protected <T> T readCharacteristic(@NonNull CameraCharacteristics characteristics,
@ -34,21 +35,53 @@ public abstract class MeteringParameter {
public final boolean isMetered() {
// A non supported parameter should always appear as metered
return isMetered || !isSupported;
return isMetered || !supportsProcessing || shouldSkip;
}
public final boolean isSuccessful() {
// A non supported parameter should always appear as successful
return isSuccessful || !isSupported;
return isSuccessful || !supportsProcessing || shouldSkip;
}
public final void startMetering(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder,
@NonNull List<MeteringRectangle> areas,
@NonNull CaptureResult lastResult,
boolean skipIfPossible) {
isSuccessful = false;
isMetered = false;
shouldSkip = skipIfPossible && checkShouldSkip(lastResult);
supportsProcessing = checkSupportsProcessing(characteristics, builder);
if (!shouldSkip) {
onStartMetering(characteristics, builder, areas, supportsProcessing);
}
}
public abstract void startMetering(@NonNull CameraCharacteristics characteristics,
protected abstract boolean checkSupportsProcessing(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder);
protected abstract boolean checkShouldSkip(@NonNull CaptureResult lastResult);
protected abstract void onStartMetering(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder,
@NonNull List<MeteringRectangle> areas);
@NonNull List<MeteringRectangle> areas,
boolean supportsProcessing);
public final void onCapture(@NonNull CaptureResult result) {
if (isMetered()) return;
processCapture(result);
}
public abstract void resetMetering(@NonNull CameraCharacteristics characteristics,
protected abstract void processCapture(@NonNull CaptureResult result);
public final void resetMetering(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder,
@NonNull MeteringRectangle area);
@Nullable MeteringRectangle area) {
onResetMetering(characteristics, builder, area, supportsProcessing);
}
public abstract void onCapture(@NonNull CaptureResult result);
protected abstract void onResetMetering(@NonNull CameraCharacteristics characteristics,
@NonNull CaptureRequest.Builder builder,
@Nullable MeteringRectangle area,
boolean supportsProcessing);
}

Loading…
Cancel
Save