diff --git a/server/src/main/java/com/genymobile/scrcpy/Command.java b/server/src/main/java/com/genymobile/scrcpy/Command.java index 0ef976a66c..362504ff0c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Command.java +++ b/server/src/main/java/com/genymobile/scrcpy/Command.java @@ -30,4 +30,14 @@ public static String execReadLine(String... cmd) throws IOException, Interrupted } return result; } + + public static String execReadOutput(String... cmd) throws IOException, InterruptedException { + Process process = Runtime.getRuntime().exec(cmd); + String output = IO.toString(process.getInputStream()); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); + } + return output; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/IO.java b/server/src/main/java/com/genymobile/scrcpy/IO.java index 57c017dbee..6eaf0d6a24 100644 --- a/server/src/main/java/com/genymobile/scrcpy/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/IO.java @@ -6,7 +6,9 @@ import java.io.FileDescriptor; import java.io.IOException; +import java.io.InputStream; import java.nio.ByteBuffer; +import java.util.Scanner; public final class IO { private IO() { @@ -37,4 +39,13 @@ public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOExcep public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException { writeFully(fd, ByteBuffer.wrap(buffer, offset, len)); } + + public static String toString(InputStream inputStream) { + StringBuilder builder = new StringBuilder(); + Scanner scanner = new Scanner(inputStream); + while (scanner.hasNextLine()) { + builder.append(scanner.nextLine()).append('\n'); + } + return builder.toString(); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index 3f4f897dfa..4a2a65beaa 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -1,8 +1,16 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Command; import com.genymobile.scrcpy.DisplayInfo; +import com.genymobile.scrcpy.Ln; import com.genymobile.scrcpy.Size; +import android.view.Display; + +import java.lang.reflect.Field; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public final class DisplayManager { private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal @@ -10,11 +18,61 @@ public DisplayManager(Object manager) { this.manager = manager; } + // public to call it from unit tests + public static DisplayInfo parseDisplayInfo(String dumpsysDisplayOutput, int displayId) { + Pattern regex = Pattern.compile( + "^ mBaseDisplayInfo=DisplayInfo\\{\".*\", displayId " + displayId + ".*(, FLAG_.*)?, real ([0-9]+) x ([0-9]+).*, rotation " + + "([0-9]+).*, layerStack ([0-9]+)", + Pattern.MULTILINE); + Matcher m = regex.matcher(dumpsysDisplayOutput); + if (!m.find()) { + return null; + } + int flags = parseDisplayFlags(m.group(1)); + int width = Integer.parseInt(m.group(2)); + int height = Integer.parseInt(m.group(3)); + int rotation = Integer.parseInt(m.group(4)); + int layerStack = Integer.parseInt(m.group(5)); + + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); + } + + private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { + try { + String dumpsysDisplayOutput = Command.execReadOutput("dumpsys", "display"); + return parseDisplayInfo(dumpsysDisplayOutput, displayId); + } catch (Exception e) { + Ln.e("Could not get display info from \"dumpsys display\" output", e); + return null; + } + } + + private static int parseDisplayFlags(String text) { + Pattern regex = Pattern.compile("FLAG_[A-Z_]+"); + if (text == null) { + return 0; + } + + int flags = 0; + Matcher m = regex.matcher(text); + while (m.find()) { + String flagString = m.group(); + try { + Field filed = Display.class.getDeclaredField(flagString); + flags |= filed.getInt(null); + } catch (NoSuchFieldException | IllegalAccessException e) { + // Silently ignore, some flags reported by "dumpsys display" are @TestApi + } + } + return flags; + } + public DisplayInfo getDisplayInfo(int displayId) { try { Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); if (displayInfo == null) { - return null; + // fallback when displayInfo is null + return getDisplayInfoFromDumpsysDisplay(displayId); } Class cls = displayInfo.getClass(); // width and height already take the rotation into account diff --git a/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java b/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java new file mode 100644 index 0000000000..3381026525 --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java @@ -0,0 +1,143 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.DisplayManager; + +import android.view.Display; + +import org.junit.Assert; +import org.junit.Test; + +public class CommandParserTest { + @Test + public void testParseDisplayInfoFromDumpsysDisplay() { + /* @formatter:off */ + String partialOutput = "Logical Displays: size=1\n" + + " Display 0:\n" + + "mDisplayId=0\n" + + " mLayerStack=0\n" + + " mHasContent=true\n" + + " mDesiredDisplayModeSpecs={baseModeId=2 primaryRefreshRateRange=[90 90] appRequestRefreshRateRange=[90 90]}\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=Built-in Screen\n" + + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, " + + "real 1440 x 3120, largest app 1440 x 3120, smallest app 1440 x 3120, appVsyncOff 2000000, presDeadline 11111111, mode 2, " + + "defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, width=1080, " + + "height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], " + + "mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, minimalPostProcessingSupported false, rotation 0, state OFF, " + + "type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 (515.154 x 514.597) dpi, layerStack 0, colorMode 0, " + + "supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo DeviceProductInfo{name=, manufacturerPnpId=QCM, " + + "productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, relativeAddress=null}, removeMode 0}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, " + + "FLAG_TRUSTED, real 1440 x 3120, largest app 3120 x 2983, smallest app 1440 x 1303, appVsyncOff 2000000, presDeadline 11111111, " + + "mode 2, defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, " + + "width=1080, height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities " + + "HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, " + + "minimalPostProcessingSupported false, rotation 0, state ON, type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 " + + "(515.154 x 514.597) dpi, layerStack 0, colorMode 0, supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo " + + "DeviceProductInfo{name=, manufacturerPnpId=QCM, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, " + + "relativeAddress=null}, removeMode 0}\n" + + " mRequestedMinimalPostProcessing=false\n"; + DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); + Assert.assertNotNull(displayInfo); + Assert.assertEquals(0, displayInfo.getDisplayId()); + Assert.assertEquals(0, displayInfo.getRotation()); + Assert.assertEquals(0, displayInfo.getLayerStack()); + // FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported + Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags()); + Assert.assertEquals(1440, displayInfo.getSize().getWidth()); + Assert.assertEquals(3120, displayInfo.getSize().getHeight()); + } + + @Test + public void testParseDisplayInfoFromDumpsysDisplayAPI31() { + /* @formatter:off */ + String partialOutput = "Logical Displays: size=1\n" + + " Display 0:\n" + + " mDisplayId=0\n" + + " mPhase=1\n" + + " mLayerStack=0\n" + + " mHasContent=true\n" + + " mDesiredDisplayModeSpecs={baseModeId=1 allowGroupSwitching=false primaryRefreshRateRange=[0 60] appRequestRefreshRateRange=[0 Infinity]}\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=Built-in Screen\n" + + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS" + + ", FLAG_TRUSTED, real 1080 x 2280, largest app 1080 x 2280, smallest app 1080 x 2280, appVsyncOff 1000000, presDeadline 16666666, " + + "mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, alternativeRefreshRates=[]}], " + + "hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, mMaxAverageLuminance=500.0, mMinLuminance=0.0}, " + + "userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state ON, type INTERNAL, uniqueId \"local:4619827259835644672\", " + + "app 1080 x 2280, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, supportedColorModes [0], address {port=0, model=0x401cec6a7a2b7b}, " + + "deviceProductInfo DeviceProductInfo{name=EMU_display_0, manufacturerPnpId=GGL, productId=1, modelYear=null, " + + "manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, removeMode 0, refreshRateOverride 0.0, " + + "brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, " + + "FLAG_TRUSTED, real 1080 x 2280, largest app 2148 x 2065, smallest app 1080 x 997, appVsyncOff 1000000, presDeadline 16666666, mode 1, " + + "defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{" + + "mSupportedHdrTypes=[], mMaxLuminance=500.0, mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], " + + "minimalPostProcessingSupported false, rotation 0, state ON, type INTERNAL, uniqueId \"local:4619827259835644672\", app 1080 x 2148, " + + "density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, supportedColorModes [0], address {port=0, model=0x401cec6a7a2b7b}, " + + "deviceProductInfo DeviceProductInfo{name=EMU_display_0, manufacturerPnpId=GGL, productId=1, modelYear=null, " + + "manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, removeMode 0, refreshRateOverride 0.0, " + + "brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + + " mRequestedMinimalPostProcessing=false\n" + + " mFrameRateOverrides=[]\n" + + " mPendingFrameRateOverrideUids={}\n"; + DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); + Assert.assertNotNull(displayInfo); + Assert.assertEquals(0, displayInfo.getDisplayId()); + Assert.assertEquals(0, displayInfo.getRotation()); + Assert.assertEquals(0, displayInfo.getLayerStack()); + // FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported + Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags()); + Assert.assertEquals(1080, displayInfo.getSize().getWidth()); + Assert.assertEquals(2280, displayInfo.getSize().getHeight()); + } + + @Test + public void testParseDisplayInfoFromDumpsysDisplayAPI31NoFlags() { + /* @formatter:off */ + String partialOutput = "Logical Displays: size=1\n" + + " Display 0:\n" + + " mDisplayId=0\n" + + " mPhase=1\n" + + " mLayerStack=0\n" + + " mHasContent=true\n" + + " mDesiredDisplayModeSpecs={baseModeId=1 allowGroupSwitching=false primaryRefreshRateRange=[0 60] appRequestRefreshRateRange=[0 Infinity]}\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=Built-in Screen\n" + + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, " + + "real 1080 x 2280, largest app 1080 x 2280, smallest app 1080 x 2280, appVsyncOff 1000000, presDeadline 16666666, " + + "mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, alternativeRefreshRates=[]}], " + + "hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, mMaxAverageLuminance=500.0, mMinLuminance=0.0}, " + + "userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state ON, type INTERNAL, uniqueId \"local:4619827259835644672\", " + + "app 1080 x 2280, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, supportedColorModes [0], address {port=0, model=0x401cec6a7a2b7b}, " + + "deviceProductInfo DeviceProductInfo{name=EMU_display_0, manufacturerPnpId=GGL, productId=1, modelYear=null, " + + "manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, removeMode 0, refreshRateOverride 0.0, " + + "brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, " + + "FLAG_TRUSTED, real 1080 x 2280, largest app 2148 x 2065, smallest app 1080 x 997, appVsyncOff 1000000, presDeadline 16666666, mode 1, " + + "defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{" + + "mSupportedHdrTypes=[], mMaxLuminance=500.0, mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], " + + "minimalPostProcessingSupported false, rotation 0, state ON, type INTERNAL, uniqueId \"local:4619827259835644672\", app 1080 x 2148, " + + "density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, supportedColorModes [0], address {port=0, model=0x401cec6a7a2b7b}, " + + "deviceProductInfo DeviceProductInfo{name=EMU_display_0, manufacturerPnpId=GGL, productId=1, modelYear=null, " + + "manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, removeMode 0, refreshRateOverride 0.0, " + + "brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + + " mRequestedMinimalPostProcessing=false\n" + + " mFrameRateOverrides=[]\n" + + " mPendingFrameRateOverrideUids={}\n"; + DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); + Assert.assertNotNull(displayInfo); + Assert.assertEquals(0, displayInfo.getDisplayId()); + Assert.assertEquals(0, displayInfo.getRotation()); + Assert.assertEquals(0, displayInfo.getLayerStack()); + Assert.assertEquals(0, displayInfo.getFlags()); + Assert.assertEquals(1080, displayInfo.getSize().getWidth()); + Assert.assertEquals(2280, displayInfo.getSize().getHeight()); + } +}