Prevent PreviewImageProcessorImpl from being called after deinit()
PreviewImageProcessorImpl could be called after deinit() because images
could still be passing through ProcessingSurfaceTexture. Added
functionality to close the adapting processors so that process() will be
skipped after closing.
Test: manual test with androidx.camera.integration.extensions app
./gradlew camera:camera-extensions:test
Change-Id: I1a866f23b5c8df4f117006a3efda382a8ac4d599
diff --git a/camera/camera-core/lint-baseline.xml b/camera/camera-core/lint-baseline.xml
index 9ffd8cd..e7dba71 100644
--- a/camera/camera-core/lint-baseline.xml
+++ b/camera/camera-core/lint-baseline.xml
@@ -2534,28 +2534,6 @@
<issue
id="UnknownNullness"
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" ListenableFuture<ImageProxy> getImageProxy(int captureId);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/core/ImageProxyBundle.java"
- line="33"
- column="5"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" List<Integer> getCaptureIds();"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/core/ImageProxyBundle.java"
- line="39"
- column="5"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
errorLine1=" Surface getSurface();"
errorLine2=" ~~~~~~~">
<location
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageProxyBundle.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageProxyBundle.java
index dd6afd7..a044ca4 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageProxyBundle.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageProxyBundle.java
@@ -31,6 +31,9 @@
* been generated.
*
* @param captureId The id for the captures that generated the {@link ImageProxy}.
+ *
+ * @return If the id does not exist for the bundle, then the ListenableFuture will contain an
+ * {@link IllegalArgumentException}.
*/
@NonNull
ListenableFuture<ImageProxy> getImageProxy(int captureId);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SingleImageProxyBundle.java b/camera/camera-core/src/main/java/androidx/camera/core/SingleImageProxyBundle.java
index 037cfdd..4c6370a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SingleImageProxyBundle.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SingleImageProxyBundle.java
@@ -17,6 +17,7 @@
package androidx.camera.core;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
import androidx.camera.core.impl.utils.futures.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -26,8 +27,11 @@
/**
* An {@link ImageProxyBundle} that contains a single {@link ImageProxy}.
+ *
+ * @hide
*/
-final class SingleImageProxyBundle implements ImageProxyBundle {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SingleImageProxyBundle implements ImageProxyBundle {
private final int mCaptureId;
private final ImageProxy mImageProxy;
@@ -37,7 +41,7 @@
*
* @throws IllegalArgumentException if the ImageProxy doesn't contain a tag
*/
- SingleImageProxyBundle(@NonNull ImageProxy imageProxy) {
+ public SingleImageProxyBundle(@NonNull ImageProxy imageProxy) {
ImageInfo imageInfo = imageProxy.getImageInfo();
if (imageInfo == null) {
diff --git a/camera/camera-extensions/build.gradle b/camera/camera-extensions/build.gradle
index 0e3202c..78717ba 100644
--- a/camera/camera-extensions/build.gradle
+++ b/camera/camera-extensions/build.gradle
@@ -38,6 +38,7 @@
testImplementation(MOCKITO_CORE)
testImplementation(ROBOLECTRIC)
+ testImplementation project(":camera:camera-testing")
testImplementation(project(":camera:camera-extensions-stub"))
// To use the extensions-stub for testing directly.
@@ -59,6 +60,9 @@
buildConfigField "String", "CAMERA_VERSION", "\"${LibraryVersions.CAMERA_EXTENSIONS}\""
}
+
+ // Use Robolectric 4.+
+ testOptions.unitTests.includeAndroidResources = true
}
// Enable generating BuildConfig.java since support library default disable it.
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/AdaptingPreviewProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/AdaptingPreviewProcessor.java
index aa930c6..19a2989 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/AdaptingPreviewProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/AdaptingPreviewProcessor.java
@@ -39,9 +39,11 @@
import java.util.concurrent.ExecutionException;
/** A {@link CaptureProcessor} that calls a vendor provided preview processing implementation. */
-final class AdaptingPreviewProcessor implements CaptureProcessor {
+final class AdaptingPreviewProcessor implements CaptureProcessor,
+ PreviewExtender.CloseableProcessor {
private static final String TAG = "AdaptingPreviewProcesso";
private final PreviewImageProcessorImpl mImpl;
+ private BlockingCloseAccessCounter mAccessCounter = new BlockingCloseAccessCounter();
AdaptingPreviewProcessor(PreviewImageProcessorImpl impl) {
mImpl = impl;
@@ -49,8 +51,16 @@
@Override
public void onOutputSurface(Surface surface, int imageFormat) {
- mImpl.onOutputSurface(surface, imageFormat);
- mImpl.onImageFormatUpdate(imageFormat);
+ if (!mAccessCounter.tryIncrement()) {
+ return;
+ }
+
+ try {
+ mImpl.onOutputSurface(surface, imageFormat);
+ mImpl.onImageFormatUpdate(imageFormat);
+ } finally {
+ mAccessCounter.decrement();
+ }
}
@Override
@@ -59,44 +69,62 @@
Preconditions.checkArgument(ids.size() == 1,
"Processing preview bundle must be 1, but found " + ids.size());
- ListenableFuture<ImageProxy> imageProxyListenableFuture = bundle.getImageProxy(ids.get(0));
+ ListenableFuture<ImageProxy> imageProxyListenableFuture = bundle.getImageProxy(
+ ids.get(0));
Preconditions.checkArgument(imageProxyListenableFuture.isDone());
+ ImageProxy imageProxy;
try {
- ImageProxy imageProxy = imageProxyListenableFuture.get();
- Image image = imageProxy.getImage();
- if (image == null) {
- return;
- }
- ImageInfo imageInfo = imageProxy.getImageInfo();
-
- CameraCaptureResult result =
- CameraCaptureResults.retrieveCameraCaptureResult(imageInfo);
- if (result == null) {
- mImpl.process(image, null);
- return;
- }
-
- CaptureResult captureResult =
- Camera2CameraCaptureResultConverter.getCaptureResult(result);
- if (captureResult == null) {
- mImpl.process(image, null);
- return;
- }
-
- if (captureResult instanceof TotalCaptureResult) {
- mImpl.process(imageProxy.getImage(), (TotalCaptureResult) captureResult);
- } else {
- mImpl.process(image, null);
- }
+ imageProxy = imageProxyListenableFuture.get();
} catch (ExecutionException | InterruptedException e) {
Log.e(TAG, "Unable to retrieve ImageProxy from bundle");
+ return;
+ }
+
+ Image image = imageProxy.getImage();
+
+ ImageInfo imageInfo = imageProxy.getImageInfo();
+ CameraCaptureResult result =
+ CameraCaptureResults.retrieveCameraCaptureResult(imageInfo);
+ CaptureResult captureResult =
+ Camera2CameraCaptureResultConverter.getCaptureResult(result);
+
+ TotalCaptureResult totalCaptureResult = null;
+ if (captureResult instanceof TotalCaptureResult) {
+ totalCaptureResult = (TotalCaptureResult) captureResult;
+ }
+
+ if (image == null) {
+ return;
+ }
+
+ if (!mAccessCounter.tryIncrement()) {
+ return;
+ }
+
+ try {
+ mImpl.process(image, totalCaptureResult);
+ } finally {
+ mAccessCounter.decrement();
}
}
@Override
public void onResolutionUpdate(Size size) {
- mImpl.onResolutionUpdate(size);
+ if (!mAccessCounter.tryIncrement()) {
+ return;
+ }
+
+ try {
+ mImpl.onResolutionUpdate(size);
+ } finally {
+ mAccessCounter.decrement();
+ }
+ }
+
+ @Override
+ public void close() {
+ mAccessCounter.destroyAndWaitForZeroAccess();
}
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/AdaptingRequestUpdateProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/AdaptingRequestUpdateProcessor.java
new file mode 100644
index 0000000..b568c2e
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/AdaptingRequestUpdateProcessor.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions;
+
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+
+import androidx.camera.camera2.impl.Camera2CameraCaptureResultConverter;
+import androidx.camera.core.CameraCaptureResult;
+import androidx.camera.core.CameraCaptureResults;
+import androidx.camera.core.CaptureStage;
+import androidx.camera.core.ImageInfo;
+import androidx.camera.core.ImageInfoProcessor;
+import androidx.camera.extensions.impl.CaptureStageImpl;
+import androidx.camera.extensions.impl.PreviewExtenderImpl;
+import androidx.camera.extensions.impl.RequestUpdateProcessorImpl;
+import androidx.core.util.Preconditions;
+
+/** A {@link ImageInfoProcessor} that calls a vendor provided preview processing implementation. */
+final class AdaptingRequestUpdateProcessor implements ImageInfoProcessor,
+ PreviewExtender.CloseableProcessor {
+ private final PreviewExtenderImpl mPreviewExtenderImpl;
+ private final RequestUpdateProcessorImpl mProcessorImpl;
+ private BlockingCloseAccessCounter mAccessCounter = new BlockingCloseAccessCounter();
+
+ AdaptingRequestUpdateProcessor(PreviewExtenderImpl previewExtenderImpl) {
+ Preconditions.checkArgument(previewExtenderImpl.getProcessorType()
+ == PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY,
+ "AdaptingRequestUpdateProcess can only adapt extender with "
+ + "PROCESSOR_TYPE_REQUEST_UPDATE_ONLY ProcessorType.");
+ mPreviewExtenderImpl = previewExtenderImpl;
+ mProcessorImpl = (RequestUpdateProcessorImpl) mPreviewExtenderImpl.getProcessor();
+ }
+
+ @Override
+ public CaptureStage getCaptureStage() {
+ if (!mAccessCounter.tryIncrement()) {
+ return null;
+ }
+
+ try {
+ return new AdaptingCaptureStage(mPreviewExtenderImpl.getCaptureStage());
+ } finally {
+ mAccessCounter.decrement();
+ }
+
+ }
+
+ @Override
+ public boolean process(ImageInfo imageInfo) {
+ if (!mAccessCounter.tryIncrement()) {
+ return false;
+ }
+
+ try {
+ boolean processResult = false;
+
+ CameraCaptureResult result = CameraCaptureResults.retrieveCameraCaptureResult(
+ imageInfo);
+ CaptureResult captureResult = Camera2CameraCaptureResultConverter.getCaptureResult(
+ result);
+
+ if (captureResult instanceof TotalCaptureResult) {
+
+ CaptureStageImpl captureStageImpl =
+ mProcessorImpl.process((TotalCaptureResult) captureResult);
+ processResult = captureStageImpl != null;
+ }
+ return processResult;
+ } finally {
+ mAccessCounter.decrement();
+ }
+ }
+
+ @Override
+ public void close() {
+ mAccessCounter.destroyAndWaitForZeroAccess();
+ }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/BlockingCloseAccessCounter.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/BlockingCloseAccessCounter.java
new file mode 100644
index 0000000..d889aec
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/BlockingCloseAccessCounter.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions;
+
+import androidx.annotation.GuardedBy;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * A counter for blocking closing until all access to the counter has been completed.
+ *
+ * <pre>{@code
+ * public void callingMethod() {
+ * if (!mAtomicAccessCounter.tryIncrement()) {
+ * return;
+ * }
+ *
+ * try {
+ * // Some work that needs to be done
+ * } finally {
+ * mAtomicAccessCounter.decrement();
+ * }
+ * }
+ *
+ * // Method that can only be called after all callingMethods are done with access
+ * public void blockingMethod() {
+ * destroyAndWaitForZeroAccess();
+ * }
+ * }</pre>
+ */
+final class BlockingCloseAccessCounter {
+ @GuardedBy("mLock")
+ private AtomicInteger mAccessCount = new AtomicInteger(0);
+ private final Lock mLock = new ReentrantLock();
+ private final Condition mDoneCondition = mLock.newCondition();
+
+ private static final int COUNTER_DESTROYED_FLAG = -1;
+
+ /**
+ * Attempt to increment the access counter.
+ *
+ * <p>Once {@link #destroyAndWaitForZeroAccess()} has returned this will always fail to
+ * increment, meaning access is not safe.
+ *
+ * @return true if the counter was incremented, false otherwise
+ */
+ boolean tryIncrement() {
+ mLock.lock();
+ try {
+ if (mAccessCount.get() == COUNTER_DESTROYED_FLAG) {
+ return false;
+ }
+ mAccessCount.getAndIncrement();
+ } finally {
+ mLock.unlock();
+ }
+ return true;
+ }
+
+ /**
+ * Decrement the access counter.
+ **/
+ void decrement() {
+ mLock.lock();
+ try {
+ switch (mAccessCount.getAndDecrement()) {
+ case COUNTER_DESTROYED_FLAG:
+ throw new IllegalStateException("Unable to decrement. Counter already "
+ + "destroyed");
+ case 0:
+ throw new IllegalStateException("Unable to decrement. No corresponding "
+ + "counter increment");
+ default:
+ //
+ }
+ mDoneCondition.signal();
+ } finally {
+ mLock.unlock();
+ }
+ }
+
+ /**
+ * Blocks until there are zero accesses in the counter.
+ *
+ * <p>Once this call completes, the counter is destroyed and can not be incremented and
+ * decremented.
+ */
+ void destroyAndWaitForZeroAccess() {
+ mLock.lock();
+
+ try {
+ while (!mAccessCount.compareAndSet(0, COUNTER_DESTROYED_FLAG)) {
+ try {
+ mDoneCondition.await();
+ } catch (InterruptedException e) {
+ // Continue to check
+ }
+ }
+ } finally {
+ mLock.unlock();
+ }
+ }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/PreviewExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/PreviewExtender.java
index 6f9beb4..3590207 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/PreviewExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/PreviewExtender.java
@@ -17,28 +17,20 @@
package androidx.camera.extensions;
import android.hardware.camera2.CameraCharacteristics;
-import android.hardware.camera2.CaptureResult;
-import android.hardware.camera2.TotalCaptureResult;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.camera.camera2.Camera2Config;
-import androidx.camera.camera2.impl.Camera2CameraCaptureResultConverter;
import androidx.camera.camera2.impl.CameraEventCallback;
import androidx.camera.camera2.impl.CameraEventCallbacks;
-import androidx.camera.core.CameraCaptureResult;
-import androidx.camera.core.CameraCaptureResults;
import androidx.camera.core.CameraIdFilter;
import androidx.camera.core.CameraIdFilterSet;
import androidx.camera.core.CameraInfoUnavailableException;
import androidx.camera.core.CameraX;
import androidx.camera.core.CaptureConfig;
-import androidx.camera.core.CaptureStage;
import androidx.camera.core.Config;
-import androidx.camera.core.ImageInfo;
-import androidx.camera.core.ImageInfoProcessor;
import androidx.camera.core.PreviewConfig;
import androidx.camera.core.UseCase;
import androidx.camera.extensions.ExtensionsErrorListener.ExtensionsErrorCode;
@@ -46,7 +38,6 @@
import androidx.camera.extensions.impl.CaptureStageImpl;
import androidx.camera.extensions.impl.PreviewExtenderImpl;
import androidx.camera.extensions.impl.PreviewImageProcessorImpl;
-import androidx.camera.extensions.impl.RequestUpdateProcessorImpl;
import java.util.Collection;
import java.util.Set;
@@ -123,18 +114,21 @@
PreviewExtenderAdapter previewExtenderAdapter;
switch (mImpl.getProcessorType()) {
case PROCESSOR_TYPE_REQUEST_UPDATE_ONLY:
- RequestUpdateProcessingExtenderAdapter requestUpdateProcessingExtenderAdapter =
- new RequestUpdateProcessingExtenderAdapter(mImpl, mEffectMode);
- mBuilder.setImageInfoProcessor(requestUpdateProcessingExtenderAdapter);
- previewExtenderAdapter = requestUpdateProcessingExtenderAdapter;
+ AdaptingRequestUpdateProcessor adaptingRequestUpdateProcessor =
+ new AdaptingRequestUpdateProcessor(mImpl);
+ mBuilder.setImageInfoProcessor(adaptingRequestUpdateProcessor);
+ previewExtenderAdapter = new PreviewExtenderAdapter(mImpl, mEffectMode,
+ adaptingRequestUpdateProcessor);
break;
case PROCESSOR_TYPE_IMAGE_PROCESSOR:
- mBuilder.setCaptureProcessor(new
- AdaptingPreviewProcessor((PreviewImageProcessorImpl) mImpl.getProcessor()));
- previewExtenderAdapter = new PreviewExtenderAdapter(mImpl, mEffectMode);
+ AdaptingPreviewProcessor adaptingPreviewProcessor = new
+ AdaptingPreviewProcessor((PreviewImageProcessorImpl) mImpl.getProcessor());
+ mBuilder.setCaptureProcessor(adaptingPreviewProcessor);
+ previewExtenderAdapter = new PreviewExtenderAdapter(mImpl, mEffectMode,
+ adaptingPreviewProcessor);
break;
default:
- previewExtenderAdapter = new PreviewExtenderAdapter(mImpl, mEffectMode);
+ previewExtenderAdapter = new PreviewExtenderAdapter(mImpl, mEffectMode, null);
}
new Camera2Config.Extender(mBuilder).setCameraEventCallback(
@@ -182,6 +176,8 @@
final PreviewExtenderImpl mImpl;
+ final CloseableProcessor mCloseableProcessor;
+
// Once the adapter has set mActive to false a new instance needs to be created
@GuardedBy("mLock")
volatile boolean mActive = true;
@@ -191,9 +187,11 @@
@GuardedBy("mLock")
private volatile boolean mUnbind = false;
- PreviewExtenderAdapter(PreviewExtenderImpl impl, EffectMode effectMode) {
+ PreviewExtenderAdapter(PreviewExtenderImpl impl, EffectMode effectMode,
+ CloseableProcessor closeableProcessor) {
mImpl = impl;
mEffectMode = effectMode;
+ mCloseableProcessor = closeableProcessor;
}
@Override
@@ -218,6 +216,9 @@
private void callDeInit() {
synchronized (mLock) {
if (mActive) {
+ if (mCloseableProcessor != null) {
+ mCloseableProcessor.close();
+ }
mImpl.onDeInit();
mActive = false;
}
@@ -302,54 +303,11 @@
}
}
- // Prevents the implementation from being accessed after deInit() has been called
- private static final class RequestUpdateProcessingExtenderAdapter extends
- PreviewExtenderAdapter implements ImageInfoProcessor {
-
- private final RequestUpdateProcessorImpl mProcessor;
-
- RequestUpdateProcessingExtenderAdapter(PreviewExtenderImpl impl, EffectMode effectMode) {
- super(impl, effectMode);
- mProcessor = ((RequestUpdateProcessorImpl) mImpl.getProcessor());
- }
-
- @Override
- public CaptureStage getCaptureStage() {
- synchronized (mLock) {
- if (mActive) {
- return new AdaptingCaptureStage(mImpl.getCaptureStage());
- }
- return null;
- }
- }
-
- @Override
- public boolean process(ImageInfo imageInfo) {
- CameraCaptureResult result =
- CameraCaptureResults.retrieveCameraCaptureResult(imageInfo);
- if (result == null) {
- return false;
- }
-
- CaptureResult captureResult =
- Camera2CameraCaptureResultConverter.getCaptureResult(result);
- if (captureResult == null) {
- return false;
- }
-
- if (captureResult instanceof TotalCaptureResult) {
- synchronized (mLock) {
- if (mActive) {
- CaptureStageImpl captureStageImpl =
- mProcessor.process((TotalCaptureResult) captureResult);
- return captureStageImpl != null;
- }
- return false;
- }
- } else {
- return false;
- }
- }
-
+ /**
+ * A processor that can be closed so that the underlying processing implementation is skipped,
+ * if it has been closed.
+ */
+ interface CloseableProcessor {
+ void close();
}
}
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/AdaptingPreviewProcessorTest.java b/camera/camera-extensions/src/test/java/androidx/camera/extensions/AdaptingPreviewProcessorTest.java
new file mode 100644
index 0000000..81ec4c5
--- /dev/null
+++ b/camera/camera-extensions/src/test/java/androidx/camera/extensions/AdaptingPreviewProcessorTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.media.Image;
+import android.os.Build;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.camera.core.ImageProxyBundle;
+import androidx.camera.core.SingleImageProxyBundle;
+import androidx.camera.extensions.impl.PreviewImageProcessorImpl;
+import androidx.camera.testing.fakes.FakeImageInfo;
+import androidx.camera.testing.fakes.FakeImageProxy;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class AdaptingPreviewProcessorTest {
+ private AdaptingPreviewProcessor mAdaptingPreviewProcessor;
+ private PreviewImageProcessorImpl mImpl;
+ private ImageProxyBundle mImageProxyBundle;
+
+ @Before
+ public void setup() {
+ mImpl = mock(PreviewImageProcessorImpl.class);
+
+ FakeImageProxy fakeImageProxy = new FakeImageProxy();
+ fakeImageProxy.setImage(mock(Image.class));
+
+ FakeImageInfo fakeImageInfo = new FakeImageInfo();
+ fakeImageInfo.setTag(1);
+
+ fakeImageProxy.setImageInfo(fakeImageInfo);
+
+ mImageProxyBundle = new SingleImageProxyBundle(fakeImageProxy);
+ mAdaptingPreviewProcessor = new AdaptingPreviewProcessor(mImpl);
+ }
+
+ @Test
+ public void processDoesNotCallImplAfterClose() {
+ mAdaptingPreviewProcessor.close();
+
+ mAdaptingPreviewProcessor.process(mImageProxyBundle);
+
+ verifyZeroInteractions(mImpl);
+ }
+
+ @Test
+ public void onImageFormatUpdateDoesNotCallImplAfterClose() {
+ mAdaptingPreviewProcessor.close();
+
+ mAdaptingPreviewProcessor.onOutputSurface(mock(Surface.class), 0);
+
+ verifyZeroInteractions(mImpl);
+ }
+
+ @Test
+ public void onResolutionUpdateDoesNotCallImplAfterClose() {
+ mAdaptingPreviewProcessor.close();
+
+ mAdaptingPreviewProcessor.onResolutionUpdate(new Size(640, 480));
+
+ verifyZeroInteractions(mImpl);
+ }
+}
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/AdaptingRequestUpdateProcessorTest.java b/camera/camera-extensions/src/test/java/androidx/camera/extensions/AdaptingRequestUpdateProcessorTest.java
new file mode 100644
index 0000000..3a694d3
--- /dev/null
+++ b/camera/camera-extensions/src/test/java/androidx/camera/extensions/AdaptingRequestUpdateProcessorTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions;
+
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.os.Build;
+
+import androidx.camera.camera2.impl.Camera2CameraCaptureResultConverter;
+import androidx.camera.core.CameraCaptureResult;
+import androidx.camera.core.CameraCaptureResults;
+import androidx.camera.core.ImageInfo;
+import androidx.camera.extensions.impl.PreviewExtenderImpl;
+import androidx.camera.extensions.impl.RequestUpdateProcessorImpl;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP,
+ shadows = {
+ AdaptingRequestUpdateProcessorTest.ShadowCameraCaptureResults.class,
+ AdaptingRequestUpdateProcessorTest.ShadowCamera2CameraCaptureResultConverter.class})
+public class AdaptingRequestUpdateProcessorTest {
+ private AdaptingRequestUpdateProcessor mAdaptingRequestUpdateProcessor;
+ private PreviewExtenderImpl mPreviewExtenderImpl;
+ private RequestUpdateProcessorImpl mImpl;
+ private ImageInfo mImageInfo;
+
+ @Before
+ public void setup() {
+ mImpl = mock(RequestUpdateProcessorImpl.class);
+ mPreviewExtenderImpl = mock(PreviewExtenderImpl.class);
+ when(mPreviewExtenderImpl.getProcessor()).thenReturn(mImpl);
+ when(mPreviewExtenderImpl.getProcessorType()).thenReturn(
+ PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY);
+
+ mImageInfo = mock(ImageInfo.class);
+
+ mAdaptingRequestUpdateProcessor = new AdaptingRequestUpdateProcessor(mPreviewExtenderImpl);
+ }
+
+ @Test
+ public void getCaptureStageDoesNotCallImplAfterClose() {
+ clearInvocations(mPreviewExtenderImpl);
+ mAdaptingRequestUpdateProcessor.close();
+
+ mAdaptingRequestUpdateProcessor.getCaptureStage();
+
+ verifyZeroInteractions(mPreviewExtenderImpl);
+ }
+
+ @Test
+ public void processDoesNotCallImplAfterClose() {
+ mAdaptingRequestUpdateProcessor.close();
+
+ mAdaptingRequestUpdateProcessor.process(mImageInfo);
+
+ verifyZeroInteractions(mImpl);
+ }
+
+ /**
+ * Shadow of {@link Camera2CameraCaptureResultConverter} to control return of
+ * {@link #getCaptureResult(CameraCaptureResult)}.
+ */
+ @Implements(
+ value = Camera2CameraCaptureResultConverter.class,
+ minSdk = 21
+ )
+ static final class ShadowCamera2CameraCaptureResultConverter {
+ /** Returns {@link TotalCaptureResult} regardless of input. */
+ @Implementation
+ public static CaptureResult getCaptureResult(CameraCaptureResult cameraCaptureResult) {
+ return mock(TotalCaptureResult.class);
+ }
+ }
+
+ /**
+ * Shadow of {@link CameraCaptureResults} to control return of
+ * {@link #retrieveCameraCaptureResult(ImageInfo)}.
+ */
+ @Implements(
+ value = CameraCaptureResults.class,
+ minSdk = 21
+ )
+ static final class ShadowCameraCaptureResults {
+ /** Returns {@link CameraCaptureResult} regardless of input. */
+ @Implementation
+ public static CameraCaptureResult retrieveCameraCaptureResult(ImageInfo imageInfo) {
+ return mock(CameraCaptureResult.class);
+ }
+ }
+
+}
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/BlockingCloseAccessCounterTest.java b/camera/camera-extensions/src/test/java/androidx/camera/extensions/BlockingCloseAccessCounterTest.java
new file mode 100644
index 0000000..7b7e8b7
--- /dev/null
+++ b/camera/camera-extensions/src/test/java/androidx/camera/extensions/BlockingCloseAccessCounterTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions;
+
+import static org.junit.Assert.assertFalse;
+
+import android.os.Build;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class BlockingCloseAccessCounterTest {
+ @Test(expected = IllegalStateException.class)
+ public void decrementWithoutIncrementThrowsException() {
+ BlockingCloseAccessCounter counter = new BlockingCloseAccessCounter();
+
+ // Expect a IllegalStateException to be thrown
+ counter.decrement();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void decrementAfterDestroy() {
+ BlockingCloseAccessCounter counter = new BlockingCloseAccessCounter();
+ counter.destroyAndWaitForZeroAccess();
+
+ // Expect a IllegalStateException to be thrown
+ counter.decrement();
+ }
+
+ @Test
+ public void incrementAfterDestroyDoesNotIncrement() {
+ BlockingCloseAccessCounter counter = new BlockingCloseAccessCounter();
+ counter.destroyAndWaitForZeroAccess();
+
+ assertFalse(counter.tryIncrement());
+ }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeImageProxy.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeImageProxy.java
index 1384b37..1f9b269 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeImageProxy.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeImageProxy.java
@@ -21,6 +21,7 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.camera.core.ImageInfo;
import androidx.camera.core.ImageProxy;
import androidx.concurrent.futures.CallbackToFutureAdapter;
@@ -105,6 +106,7 @@
}
@Override
+ @Nullable
public Image getImage() {
return mImage;
}
@@ -129,6 +131,10 @@
mImageInfo = imageInfo;
}
+ public void setImage(@Nullable Image image) {
+ mImage = image;
+ }
+
/**
* Returns ListenableFuture that completes when the {@link FakeImageProxy} has closed.
*/