From 284fe294ac152dc98f9a053c77ab5f7d1d1c6303 Mon Sep 17 00:00:00 2001 From: Curt Brune Date: Fri, 13 Dec 2019 14:01:13 -0800 Subject: [PATCH] Skip monochrome cameras when switching cameras on video calls. --- app/build.gradle | 2 +- app/witness-verifications.gradle | 4 +- .../ringrtc/CallConnectionWrapper.java | 114 +++++++++++++++++- 3 files changed, 116 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1d819e4d7..7d7d16773 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -117,7 +117,7 @@ dependencies { implementation project(':libsignal-service') - implementation 'org.signal:ringrtc-android:0.3.0' + implementation 'org.signal:ringrtc-android:0.3.1' implementation "me.leolin:ShortcutBadger:1.1.16" implementation 'se.emilsjolander:stickylistheaders:2.7.0' diff --git a/app/witness-verifications.gradle b/app/witness-verifications.gradle index 231c666c2..ca7689599 100644 --- a/app/witness-verifications.gradle +++ b/app/witness-verifications.gradle @@ -351,8 +351,8 @@ dependencyVerification { ['org.signal:android-database-sqlcipher:3.5.9-S3', '33d4063336893af00b9d68b418e7b290cace74c20ce8aacffddc0911010d3d73'], - ['org.signal:ringrtc-android:0.3.0', - 'c3c294915f86d6dbaf934d6542e3f99a003c50f5490bd2c0b80f5fe3a7a5fdfc'], + ['org.signal:ringrtc-android:0.3.1', + '785c422a2322f810141d85f63eab3874961b19face886daaf140ab011d39e166'], ['org.signal:signal-metadata-java:0.1.0', 'f3faa23b7d9b5096d12979c35679d1e3b5e007522d8bef167a28e456f2a7c7d9'], diff --git a/src/org/thoughtcrime/securesms/ringrtc/CallConnectionWrapper.java b/src/org/thoughtcrime/securesms/ringrtc/CallConnectionWrapper.java index 614b2a7fe..b40d55298 100644 --- a/src/org/thoughtcrime/securesms/ringrtc/CallConnectionWrapper.java +++ b/src/org/thoughtcrime/securesms/ringrtc/CallConnectionWrapper.java @@ -1,11 +1,20 @@ package org.thoughtcrime.securesms.ringrtc; +import android.annotation.TargetApi; import android.content.Context; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.annimon.stream.Stream; + import java.io.IOException; +import java.util.List; +import java.util.LinkedList; import org.signal.ringrtc.CallConnection; import org.signal.ringrtc.CallConnectionFactory; @@ -17,6 +26,7 @@ import org.thoughtcrime.securesms.logging.Log; import org.webrtc.AudioSource; import org.webrtc.AudioTrack; import org.webrtc.Camera1Enumerator; +import org.webrtc.Camera2Capturer; import org.webrtc.Camera2Enumerator; import org.webrtc.CameraEnumerator; import org.webrtc.CameraVideoCapturer; @@ -265,7 +275,7 @@ public class CallConnectionWrapper { Log.i(TAG, "Camera2 enumerator supported: " + camera2EnumeratorIsSupported); - return camera2EnumeratorIsSupported ? new Camera2Enumerator(context) + return camera2EnumeratorIsSupported ? new FilteredCamera2Enumerator(context) : new Camera1Enumerator(true); } @@ -285,4 +295,106 @@ public class CallConnectionWrapper { public interface CameraEventListener { void onCameraSwitchCompleted(@NonNull CameraState newCameraState); } + + @TargetApi(21) + private static class FilteredCamera2Enumerator extends Camera2Enumerator { + + @NonNull private final Context context; + @Nullable private final CameraManager cameraManager; + @Nullable private String[] deviceNames; + + FilteredCamera2Enumerator(@NonNull Context context) { + super(context); + + this.context = context; + this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); + this.deviceNames = null; + } + + private boolean isMonochrome(String deviceName, CameraManager cameraManager) { + + try { + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(deviceName); + int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES); + + if (capabilities != null) { + for (int cap : capabilities) { + if (cap == CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME) { + return true; + } + } + } + } catch (CameraAccessException e) { + return false; + } + + return false; + } + + private boolean isLensFacing(String deviceName, CameraManager cameraManager, Integer facing) { + + try { + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(deviceName); + Integer lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING); + + return facing.equals(lensFacing); + } catch (CameraAccessException e) { + return false; + } + + } + + @Override + public @NonNull String[] getDeviceNames() { + + if (deviceNames != null) { + return deviceNames; + } + + try { + List cameraList = new LinkedList<>(); + + if (cameraManager != null) { + // While skipping cameras that are monochrome, gather cameras + // until we have at most 1 front facing camera and 1 back + // facing camera. + + List devices = Stream.of(cameraManager.getCameraIdList()) + .filterNot(id -> isMonochrome(id, cameraManager)) + .toList(); + + String frontCamera = Stream.of(devices) + .filter(id -> isLensFacing(id, cameraManager, CameraMetadata.LENS_FACING_FRONT)) + .findFirst() + .orElse(null); + + if (frontCamera != null) { + cameraList.add(frontCamera); + } + + String backCamera = Stream.of(devices) + .filter(id -> isLensFacing(id, cameraManager, CameraMetadata.LENS_FACING_BACK)) + .findFirst() + .orElse(null); + + if (backCamera != null) { + cameraList.add(backCamera); + } + } + + this.deviceNames = cameraList.toArray(new String[0]); + } catch (CameraAccessException e) { + Log.e(TAG, "Camera access exception: " + e); + this.deviceNames = new String[] {}; + } + + return deviceNames; + } + + @Override + public @NonNull CameraVideoCapturer createCapturer(@Nullable String deviceName, + @Nullable CameraVideoCapturer.CameraEventsHandler eventsHandler) { + return new Camera2Capturer(context, deviceName, eventsHandler, new FilteredCamera2Enumerator(context)); + } + } }