diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index d16592b49a..3c47f59f43 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -57,6 +57,7 @@ cd "$SERVER_DIR/src/main/aidl" # Fake sources to expose hidden Android types to the project FAKE_SRC=( \ android/content/*java \ + android/hardware/*java \ ) SRC=( \ diff --git a/server/src/main/java/android/hardware/ICameraServiceListener.java b/server/src/main/java/android/hardware/ICameraServiceListener.java new file mode 100644 index 0000000000..344fe98c53 --- /dev/null +++ b/server/src/main/java/android/hardware/ICameraServiceListener.java @@ -0,0 +1,14 @@ +package android.hardware; + +import android.os.IBinder; + +public interface ICameraServiceListener { + int STATUS_NOT_PRESENT = 0; + int STATUS_ENUMERATING = 2; + + class Default implements ICameraServiceListener { + public IBinder asBinder() { + return null; + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 088be7e7d2..f4d62d01e3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -7,6 +7,7 @@ import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.video.VideoCodec; +import com.genymobile.scrcpy.wrappers.CameraService; import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; @@ -23,6 +24,7 @@ import android.os.Build; import android.util.Range; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -122,56 +124,80 @@ private static String getCameraFacingName(int facing) { public static String buildCameraListMessage(boolean includeSizes) { StringBuilder builder = new StringBuilder("List of cameras:"); + CameraService cameraService = ServiceManager.getCameraService(); CameraManager cameraManager = ServiceManager.getCameraManager(); try { - String[] cameraIds = cameraManager.getCameraIdList(); + String[] cameraIds = cameraService.getCameraIdList(); if (cameraIds == null || cameraIds.length == 0) { builder.append("\n (none)"); } else { for (String id : cameraIds) { - builder.append("\n --camera-id=").append(id); - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); + try { + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); - int facing = characteristics.get(CameraCharacteristics.LENS_FACING); - builder.append(" (").append(getCameraFacingName(facing)).append(", "); + int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES); + if (capabilities == null) { + continue; + } + // Ignore depth cameras as suggested by official documentation + // + boolean isBackwardCompatible = false; + for (int capability : capabilities) { + if (capability == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) { + isBackwardCompatible = true; + break; + } + } + if (!isBackwardCompatible) { + continue; + } - Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - builder.append(activeSize.width()).append("x").append(activeSize.height()); + builder.append("\n --camera-id=").append(id); - try { - // Capture frame rates for low-FPS mode are the same for every resolution - Range[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - SortedSet uniqueLowFps = getUniqueSet(lowFpsRanges); - builder.append(", fps=").append(uniqueLowFps); - } catch (Exception e) { - // Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper" - Ln.w("Could not get available frame rates for camera " + id, e); - } + int facing = characteristics.get(CameraCharacteristics.LENS_FACING); + builder.append(" (").append(getCameraFacingName(facing)).append(", "); + + Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + builder.append(activeSize.width()).append("x").append(activeSize.height()); - builder.append(')'); + try { + // Capture frame rates for low-FPS mode are the same for every resolution + Range[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + SortedSet uniqueLowFps = getUniqueSet(lowFpsRanges); + builder.append(", fps=").append(uniqueLowFps); + } catch (Exception e) { + // Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper" + Ln.w("Could not get available frame rates for camera " + id, e); + } - if (includeSizes) { - StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + builder.append(')'); - android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class); - if (sizes == null || sizes.length == 0) { - builder.append("\n (none)"); - } else { - for (android.util.Size size : sizes) { - builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight()); + if (includeSizes) { + StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + + android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class); + if (sizes == null || sizes.length == 0) { + builder.append("\n (none)"); + } else { + for (android.util.Size size : sizes) { + builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight()); + } } - } - android.util.Size[] highSpeedSizes = configs.getHighSpeedVideoSizes(); - if (highSpeedSizes != null && highSpeedSizes.length > 0) { - builder.append("\n High speed capture (--camera-high-speed):"); - for (android.util.Size size : highSpeedSizes) { - Range[] highFpsRanges = configs.getHighSpeedVideoFpsRanges(); - SortedSet uniqueHighFps = getUniqueSet(highFpsRanges); - builder.append("\n - ").append(size.getWidth()).append("x").append(size.getHeight()); - builder.append(" (fps=").append(uniqueHighFps).append(')'); + android.util.Size[] highSpeedSizes = configs.getHighSpeedVideoSizes(); + if (highSpeedSizes != null && highSpeedSizes.length > 0) { + builder.append("\n High speed capture (--camera-high-speed):"); + for (android.util.Size size : highSpeedSizes) { + Range[] highFpsRanges = configs.getHighSpeedVideoFpsRanges(); + SortedSet uniqueHighFps = getUniqueSet(highFpsRanges); + builder.append("\n - ").append(size.getWidth()).append("x").append(size.getHeight()); + builder.append(" (fps=").append(uniqueHighFps).append(')'); + } } } + } catch (IllegalArgumentException ignore) { + // Samsung devices might throw an IllegalArgumentException + // when getting camera characteristics for hidden cameras } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 0e147cb718..94eb4803a2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -12,6 +12,7 @@ import com.genymobile.scrcpy.util.HandlerExecutor; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.wrappers.CameraService; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; @@ -140,9 +141,10 @@ public void prepare() throws IOException { } private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException, ConfigurationException { + CameraService cameraService = ServiceManager.getCameraService(); CameraManager cameraManager = ServiceManager.getCameraManager(); - String[] cameraIds = cameraManager.getCameraIdList(); + String[] cameraIds = cameraService.getCameraIdList(); if (explicitCameraId != null) { if (!Arrays.asList(cameraIds).contains(explicitCameraId)) { Ln.e("Camera with id " + explicitCameraId + " not found\n" + LogUtils.buildCameraListMessage(false)); @@ -157,11 +159,16 @@ private static String selectCamera(String explicitCameraId, CameraFacing cameraF } for (String cameraId : cameraIds) { - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); + try { + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); - int facing = characteristics.get(CameraCharacteristics.LENS_FACING); - if (cameraFacing.value() == facing) { - return cameraId; + int facing = characteristics.get(CameraCharacteristics.LENS_FACING); + if (cameraFacing.value() == facing) { + return cameraId; + } + } catch (IllegalArgumentException ignore) { + // Samsung devices might throw an IllegalArgumentException + // when getting camera characteristics for hidden cameras } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/CameraService.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/CameraService.java new file mode 100644 index 0000000000..c2937c4137 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/CameraService.java @@ -0,0 +1,110 @@ +package com.genymobile.scrcpy.wrappers; + +import android.annotation.SuppressLint; +import android.hardware.ICameraServiceListener; +import android.os.Binder; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Parcel; +import android.util.Log; + +import com.genymobile.scrcpy.util.Ln; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +public class CameraService { + private final IInterface service; + + private Method addListenerMethod; + private Method removeListenerMethod; + + public CameraService(IInterface service) { + this.service = service; + } + + private Method getAddListenerMethod() throws NoSuchMethodException { + if (addListenerMethod == null) { + addListenerMethod = service.getClass().getDeclaredMethod("addListener", ICameraServiceListener.class); + } + return addListenerMethod; + } + + public CameraStatus[] addListener(ICameraServiceListener listener) { + try { + Object[] rawStatuses = (Object[]) getAddListenerMethod().invoke(service, listener); + if (rawStatuses == null) { + return null; + } + CameraStatus[] cameraStatuses = new CameraStatus[rawStatuses.length]; + for (int i = 0; i < rawStatuses.length; i++) { + cameraStatuses[i] = new CameraStatus(rawStatuses[i]); + } + return cameraStatuses; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private Method getRemoveListenerMethod() throws NoSuchMethodException { + if (removeListenerMethod == null) { + removeListenerMethod = service.getClass().getDeclaredMethod("removeListener", ICameraServiceListener.class); + } + return removeListenerMethod; + } + + public void removeListener(ICameraServiceListener listener) { + try { + getRemoveListenerMethod().invoke(service, listener); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + // Simulate `CameraManager.getCameraIdList()` + // + public String[] getCameraIdList() { + Binder binder = new Binder() { + @Override + protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) { + return true; + } + }; + ICameraServiceListener listener = new ICameraServiceListener.Default() { + // To immune from future changes to `ICameraServiceListener`, + // we use an empty `Binder` that ignores all transactions. + @Override + public IBinder asBinder() { + return binder; + } + }; + + CameraStatus[] cameraStatuses = addListener(listener); + removeListener(listener); + + if (cameraStatuses == null) { + return null; + } + + List cameraIds = new ArrayList<>(); + for (CameraStatus cameraStatus : cameraStatuses) { + if (cameraStatus.status != ICameraServiceListener.STATUS_NOT_PRESENT && cameraStatus.status != ICameraServiceListener.STATUS_ENUMERATING) { + cameraIds.add(cameraStatus.cameraId); + } + } + + return cameraIds.toArray(new String[0]); + } + + @SuppressLint({"SoonBlockedPrivateApi", "PrivateApi"}) + public static class CameraStatus { + public int status; + public String cameraId; + + public CameraStatus(Object raw) throws NoSuchFieldException, IllegalAccessException { + status = raw.getClass().getDeclaredField("status").getInt(raw); + cameraId = (String) raw.getClass().getDeclaredField("cameraId").get(raw); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index a8a56dabb5..c686479317 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -31,6 +31,7 @@ public final class ServiceManager { private static StatusBarManager statusBarManager; private static ClipboardManager clipboardManager; private static ActivityManager activityManager; + private static CameraService cameraService; private static CameraManager cameraManager; private ServiceManager() { @@ -97,6 +98,18 @@ public static ActivityManager getActivityManager() { return activityManager; } + public static CameraService getCameraService() { + if (cameraService == null) { + try { + IInterface camera = getService("media.camera", "android.hardware.ICameraService"); + cameraService = new CameraService(camera); + } catch (Exception e) { + throw new AssertionError(e); + } + } + return cameraService; + } + public static CameraManager getCameraManager() { if (cameraManager == null) { try {