Compare commits

...

3 Commits

Author SHA1 Message Date
Kaiming Hu
7a86156503 Support custom virtual display refresh rates
Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-11-20 20:11:17 +01:00
Romain Vimont
1b5d88368a Keep DisplayManager instance across calls
Do not create a new android.hardware.display.DisplayManager for every
creation of a new virtual display.
2024-11-20 20:11:03 +01:00
Romain Vimont
daba00a819 Dissociate virtual display size and capture size
Allow capturing virtual displays at a lower resolution using
-m/--max-size.

In the original implementation in #5370, the virtual display size was
necessarily the same as the capture size. The --max-size value was only
allowed to determine the virtual display size when no explicit size was
provided.

Since the dpi was scaled down accordingly, it is often better to create
a virtual display at the target capture size directly. However, not
everything is rendered according to the virtual display DPI. For
example, a page in Firefox is rendered too big on small virtual
displays. Thus, it makes sense to be able create a virtual display at a
given size, and capture it at a lower resolution with --max-size. This
is now possible using OpenGL filters.

Therefore, change the behavior of --max-size for virtual displays:
 - it does not impact --new-display without size argument anymore (the
   virtual display size is the main display size);
 - it is used to limit the capture size (whether an explicit size is
   provided or not).

This new behavior is consistent with main display capture.

Refs #5370 comment <https://github.com/Genymobile/scrcpy/pull/5370#issuecomment-2438944401>
Refs <https://github.com/Genymobile/scrcpy/pull/5370>
2024-11-20 13:01:57 +01:00
11 changed files with 277 additions and 60 deletions

View File

@ -318,14 +318,14 @@ Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video
.TP .TP
\fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]] \fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]]
Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI, and \fB\-\-max\-size\fR is considered. Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI.
Examples: Examples:
\-\-new\-display=1920x1080 \-\-new\-display=1920x1080
\-\-new\-display=1920x1080/420 \-\-new\-display=1920x1080/420 # force 420 dpi
\-\-new\-display=1920x1080@24 # 24 fps (Android >= 14)
\-\-new\-display # main display size and density \-\-new\-display # main display size and density
\-\-new\-display -m1920 # scaled to fit a max size of 1920
\-\-new\-display=/240 # main display size and 240 dpi \-\-new\-display=/240 # main display size and 240 dpi
.TP .TP

View File

@ -586,16 +586,18 @@ static const struct sc_option options[] = {
{ {
.longopt_id = OPT_NEW_DISPLAY, .longopt_id = OPT_NEW_DISPLAY,
.longopt = "new-display", .longopt = "new-display",
.argdesc = "[<width>x<height>][/<dpi>]", .argdesc = "[<width>x<height>][/<dpi>][@<fps>]",
.optional_arg = true, .optional_arg = true,
.text = "Create a new display with the specified resolution and " .text = "Create a new display with the specified resolution and "
"density. If not provided, they default to the main display " "density. If not provided, they default to the main display "
"dimensions and DPI, and --max-size is considered.\n" "dimensions and DPI.\n"
"From Android 14, it is also possible to request a frame rate. "
"If not provided, it defaults to 60 fps.\n"
"Examples:\n" "Examples:\n"
" --new-display=1920x1080\n" " --new-display=1920x1080\n"
" --new-display=1920x1080/420 # force 420 dpi\n" " --new-display=1920x1080/420 # force 420 dpi\n"
" --new-display=1920x1080@24 # 24 fps (Android >= 14)\n"
" --new-display # main display size and density\n" " --new-display # main display size and density\n"
" --new-display -m1920 # scaled to fit a max size of 1920\n"
" --new-display=/240 # main display size and 240 dpi", " --new-display=/240 # main display size and 240 dpi",
}, },
{ {
@ -2891,13 +2893,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
LOGE("--new-display is incompatible with --no-video"); LOGE("--new-display is incompatible with --no-video");
return false; return false;
} }
if (opts->max_size && opts->new_display[0] != '\0'
&& opts->new_display[0] != '/') {
// An explicit size is defined (not "" nor "/<dpi>")
LOGE("Cannot specify both --new-display size and -m/--max-size");
return false;
}
} }
if (otg) { if (otg) {

View File

@ -193,9 +193,9 @@ phone, landscape for a tablet).
Cropping is performed before `--capture-orientation` and `--angle`. Cropping is performed before `--capture-orientation` and `--angle`.
For screen mirroring, `--max-size` is applied after cropping. For camera and For display mirroring, `--max-size` is applied after cropping. For camera,
virtual display mirroring, `--max-size` is applied first (because it selects the `--max-size` is applied first (because it selects the source size rather than
source size rather than resizing it). resizing the content).
## Display ## Display

View File

@ -7,8 +7,8 @@ To mirror a new virtual display instead of the device screen:
```bash ```bash
scrcpy --new-display=1920x1080 scrcpy --new-display=1920x1080
scrcpy --new-display=1920x1080/420 # force 420 dpi scrcpy --new-display=1920x1080/420 # force 420 dpi
scrcpy --new-display=1920x1080@24 # 24 fps (Android >= 14)
scrcpy --new-display # use the main display size and density scrcpy --new-display # use the main display size and density
scrcpy --new-display -m1920 # ... scaled to fit a max size of 1920
scrcpy --new-display=/240 # use the main display size and 240 dpi scrcpy --new-display=/240 # use the main display size and 240 dpi
``` ```

View File

@ -566,36 +566,68 @@ public class Options {
} }
} }
private static NewDisplay parseNewDisplay(String newDisplay) { static NewDisplay parseNewDisplay(String newDisplay) {
// Possible inputs: // Input in the form "[<width>x<height>][/<dpi>][@<fps>]" (each [] block is optional)
// - "" (empty string) // For convenience, the order of dpi and fps does not matter.
// - "<width>x<height>/<dpi>"
// - "<width>x<height>"
// - "/<dpi>"
if (newDisplay.isEmpty()) { if (newDisplay.isEmpty()) {
return new NewDisplay(); return new NewDisplay();
} }
String[] tokens = newDisplay.split("/"); String sizeString = null;
String dpiString = null;
String fpsString = null;
Size size; String s = newDisplay;
if (!tokens[0].isEmpty()) { while (true) {
size = parseSize(tokens[0]); int slashIndex = s.indexOf('/');
} else { int atIndex = s.indexOf('@');
size = null; int lastSepIndex = Math.max(slashIndex, atIndex);
} if (lastSepIndex == -1) {
if (!s.isEmpty()) {
int dpi; sizeString = s;
if (tokens.length >= 2) { }
dpi = Integer.parseInt(tokens[1]); break;
if (dpi <= 0) { } else {
throw new IllegalArgumentException("Invalid non-positive dpi: " + tokens[1]); char lastSep = newDisplay.charAt(lastSepIndex);
if (lastSep == '@') {
if (fpsString != null) {
throw new IllegalArgumentException("Invalid new display format: '@' may not appear twice");
}
fpsString = s.substring(lastSepIndex + 1);
} else {
assert lastSep == '/';
if (dpiString != null) {
throw new IllegalArgumentException("Invalid new display format: '/' may not appear twice");
}
dpiString = s.substring(lastSepIndex + 1);
}
s = s.substring(0, lastSepIndex);
} }
} else {
dpi = 0;
} }
return new NewDisplay(size, dpi); Size size = null;
int dpi = 0;
float fps = 0;
if (sizeString != null) {
size = parseSize(sizeString);
}
if (dpiString != null) {
dpi = Integer.parseInt(dpiString);
if (dpi <= 0) {
throw new IllegalArgumentException("Invalid non-positive dpi: " + dpiString);
}
}
if (fpsString != null) {
fps = Float.parseFloat(fpsString);
if (fps < 0) {
throw new IllegalArgumentException("Invalid negative fps: " + fpsString);
}
}
return new NewDisplay(size, dpi, fps);
} }
private static Pair<Orientation.Lock, Orientation> parseCaptureOrientation(String value) { private static Pair<Orientation.Lock, Orientation> parseCaptureOrientation(String value) {

View File

@ -3,14 +3,16 @@ package com.genymobile.scrcpy.device;
public final class NewDisplay { public final class NewDisplay {
private Size size; private Size size;
private int dpi; private int dpi;
private float fps;
public NewDisplay() { public NewDisplay() {
// Auto size and dpi // Auto size, dpi and fps
} }
public NewDisplay(Size size, int dpi) { public NewDisplay(Size size, int dpi, float fps) {
this.size = size; this.size = size;
this.dpi = dpi; this.dpi = dpi;
this.fps = fps;
} }
public Size getSize() { public Size getSize() {
@ -21,6 +23,10 @@ public final class NewDisplay {
return dpi; return dpi;
} }
public float getFps() {
return fps;
}
public boolean hasExplicitSize() { public boolean hasExplicitSize() {
return size != null; return size != null;
} }
@ -28,4 +34,8 @@ public final class NewDisplay {
public boolean hasExplicitDpi() { public boolean hasExplicitDpi() {
return dpi != 0; return dpi != 0;
} }
public boolean hasExplicitFps() {
return fps != 0;
}
} }

View File

@ -60,7 +60,7 @@ public final class Size {
* @return The current size rounded. * @return The current size rounded.
*/ */
public Size round8() { public Size round8() {
if ((width & 7) == 0 && (height & 7) == 0) { if (isMultipleOf8()) {
// Already a multiple of 8 // Already a multiple of 8
return this; return this;
} }
@ -80,6 +80,10 @@ public final class Size {
return new Size(w, h); return new Size(w, h);
} }
public boolean isMultipleOf8() {
return (width & 7) == 0 && (height & 7) == 0;
}
public Rect toRect() { public Rect toRect() {
return new Rect(0, 0, width, height); return new Rect(0, 0, width, height);
} }

View File

@ -14,8 +14,10 @@ import com.genymobile.scrcpy.util.AffineMatrix;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.graphics.Rect; import android.graphics.Rect;
import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplay;
import android.hardware.display.VirtualDisplayConfig;
import android.os.Build; import android.os.Build;
import android.view.Surface; import android.view.Surface;
@ -48,7 +50,7 @@ public class NewDisplayCapture extends SurfaceCapture {
private Size mainDisplaySize; private Size mainDisplaySize;
private int mainDisplayDpi; private int mainDisplayDpi;
private int maxSize; // only used if newDisplay.getSize() != null private int maxSize;
private final Rect crop; private final Rect crop;
private final boolean captureOrientationLocked; private final boolean captureOrientationLocked;
private final Orientation captureOrientation; private final Orientation captureOrientation;
@ -101,7 +103,7 @@ public class NewDisplayCapture extends SurfaceCapture {
int displayRotation; int displayRotation;
if (virtualDisplay == null) { if (virtualDisplay == null) {
if (!newDisplay.hasExplicitSize()) { if (!newDisplay.hasExplicitSize()) {
displaySize = mainDisplaySize.limit(maxSize).round8(); displaySize = mainDisplaySize;
} }
if (!newDisplay.hasExplicitDpi()) { if (!newDisplay.hasExplicitDpi()) {
dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, displaySize); dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, displaySize);
@ -128,10 +130,19 @@ public class NewDisplayCapture extends SurfaceCapture {
filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation); filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation);
filter.addAngle(angle); filter.addAngle(angle);
Size filteredSize = filter.getOutputSize();
if (!filteredSize.isMultipleOf8() || (maxSize != 0 && filteredSize.getMax() > maxSize)) {
if (maxSize != 0) {
filteredSize = filteredSize.limit(maxSize);
}
filteredSize = filteredSize.round8();
filter.addResize(filteredSize);
}
eventTransform = filter.getInverseTransform(); eventTransform = filter.getInverseTransform();
// DisplayInfo gives the oriented size (so videoSize includes the display rotation) // DisplayInfo gives the oriented size (so videoSize includes the display rotation)
videoSize = filter.getOutputSize().limit(maxSize).round8(); videoSize = filter.getOutputSize();
// But the virtual display video always remains in the origin orientation (the video itself is not rotated, so it must rotated manually). // But the virtual display video always remains in the origin orientation (the video itself is not rotated, so it must rotated manually).
// This additional display rotation must not be included in the input events transform (the expected coordinates are already in the // This additional display rotation must not be included in the input events transform (the expected coordinates are already in the
@ -152,6 +163,7 @@ public class NewDisplayCapture extends SurfaceCapture {
displayTransform = AffineMatrix.multiplyAll(displayRotationMatrix, eventTransform); displayTransform = AffineMatrix.multiplyAll(displayRotationMatrix, eventTransform);
} }
@SuppressLint("WrongConstant")
public void startNew(Surface surface) { public void startNew(Surface surface) {
int virtualDisplayId; int virtualDisplayId;
try { try {
@ -173,10 +185,30 @@ public class NewDisplayCapture extends SurfaceCapture {
| VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
} }
} }
virtualDisplay = ServiceManager.getDisplayManager()
.createNewVirtualDisplay("scrcpy", displaySize.getWidth(), displaySize.getHeight(), dpi, surface, flags); // Since Android 14, it is possible to request a display frame rate:
// <https://android.googlesource.com/platform/frameworks/base/+/6c57176e9a2882eff03c5b3f3cccfd988d38488d>
// It defaults to 60 fps:
// <https://android.googlesource.com/platform/frameworks/base/+/6c57176e9a2882eff03c5b3f3cccfd988d38488d/services/core/java/com/android/server/display/VirtualDisplayAdapter.java#562>
float fps = newDisplay.getFps();
if (fps > 0) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
"scrcpy", displaySize.getWidth(), displaySize.getHeight(), dpi);
builder.setFlags(flags);
builder.setSurface(surface);
builder.setRequestedRefreshRate(fps);
virtualDisplay = ServiceManager.getDisplayManager().createNewVirtualDisplay(builder.build());
} else {
throw new UnsupportedOperationException("Setting the virtual display frame rate (@" + fps + ") requires Android >= 14");
}
} else {
virtualDisplay = ServiceManager.getDisplayManager()
.createNewVirtualDisplay("scrcpy", displaySize.getWidth(), displaySize.getHeight(), dpi, surface, flags);
}
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); String fpsString = fps > 0 ? "@" + fps : "";
Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + fpsString + " (id=" + virtualDisplayId + ")");
displaySizeMonitor.start(virtualDisplayId, this::invalidate); displaySizeMonitor.start(virtualDisplayId, this::invalidate);
} catch (Exception e) { } catch (Exception e) {
@ -231,11 +263,6 @@ public class NewDisplayCapture extends SurfaceCapture {
@Override @Override
public synchronized boolean setMaxSize(int newMaxSize) { public synchronized boolean setMaxSize(int newMaxSize) {
if (newDisplay.hasExplicitSize()) {
// Cannot retry with a different size if the display size was explicitly provided
return false;
}
maxSize = newMaxSize; maxSize = newMaxSize;
return true; return true;
} }

View File

@ -103,4 +103,17 @@ public class VideoFilter {
double ccwAngle = -cwAngle; double ccwAngle = -cwAngle;
transform = AffineMatrix.rotate(ccwAngle).withAspectRatio(size).fromCenter().multiply(transform); transform = AffineMatrix.rotate(ccwAngle).withAspectRatio(size).fromCenter().multiply(transform);
} }
public void addResize(Size targetSize) {
if (size.equals(targetSize)) {
return;
}
if (transform == null) {
// The requested scaling is performed by the viewport (by changing the output size), but the OpenGL filter must still run, even if
// resizing is not performed by the shader. So transform MUST NOT be null.
transform = AffineMatrix.IDENTITY;
}
size = targetSize;
}
} }

View File

@ -11,6 +11,7 @@ import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplay;
import android.hardware.display.VirtualDisplayConfig;
import android.os.Handler; import android.os.Handler;
import android.view.Display; import android.view.Display;
import android.view.Surface; import android.view.Surface;
@ -46,6 +47,7 @@ public final class DisplayManager {
} }
private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
private android.hardware.display.DisplayManager displayManager;
private Method createVirtualDisplayMethod; private Method createVirtualDisplayMethod;
private Method requestDisplayPowerMethod; private Method requestDisplayPowerMethod;
@ -151,17 +153,31 @@ public final class DisplayManager {
return createVirtualDisplayMethod; return createVirtualDisplayMethod;
} }
public VirtualDisplay createVirtualDisplay(String name, int width, int height, int displayIdToMirror, Surface surface) throws Exception { public VirtualDisplay createVirtualDisplay(String name, int width, int height, int displayIdToMirror, Surface surface)
throws ReflectiveOperationException {
Method method = getCreateVirtualDisplayMethod(); Method method = getCreateVirtualDisplayMethod();
return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface); return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface);
} }
public VirtualDisplay createNewVirtualDisplay(String name, int width, int height, int dpi, Surface surface, int flags) throws Exception { private android.hardware.display.DisplayManager getAndroidDisplayManager() throws ReflectiveOperationException {
Constructor<android.hardware.display.DisplayManager> ctor = android.hardware.display.DisplayManager.class.getDeclaredConstructor( if (displayManager == null) {
Context.class); Constructor<android.hardware.display.DisplayManager> ctor = android.hardware.display.DisplayManager.class.getDeclaredConstructor(
ctor.setAccessible(true); Context.class);
android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get()); ctor.setAccessible(true);
return dm.createVirtualDisplay(name, width, height, dpi, surface, flags); displayManager = ctor.newInstance(FakeContext.get());
}
return displayManager;
}
public VirtualDisplay createNewVirtualDisplay(String name, int width, int height, int dpi, Surface surface, int flags)
throws ReflectiveOperationException {
return getAndroidDisplayManager().createVirtualDisplay(name, width, height, dpi, surface, flags);
}
@TargetApi(AndroidVersions.API_34_ANDROID_14)
public VirtualDisplay createNewVirtualDisplay(VirtualDisplayConfig config) throws ReflectiveOperationException {
return getAndroidDisplayManager().createVirtualDisplay(config);
} }
private Method getRequestDisplayPowerMethod() throws NoSuchMethodException { private Method getRequestDisplayPowerMethod() throws NoSuchMethodException {

View File

@ -0,0 +1,120 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.device.NewDisplay;
import com.genymobile.scrcpy.device.Size;
import org.junit.Assert;
import org.junit.Test;
public class OptionsTest {
@Test
public void testParseNewDisplayEmpty() {
NewDisplay newDisplay = Options.parseNewDisplay("");
Assert.assertFalse(newDisplay.hasExplicitSize());
Assert.assertFalse(newDisplay.hasExplicitDpi());
Assert.assertFalse(newDisplay.hasExplicitFps());
Assert.assertNull(newDisplay.getSize());
Assert.assertEquals(0, newDisplay.getDpi());
Assert.assertEquals(0, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplaySizeOnly() {
NewDisplay newDisplay = Options.parseNewDisplay("1920x1080");
Assert.assertTrue(newDisplay.hasExplicitSize());
Assert.assertFalse(newDisplay.hasExplicitDpi());
Assert.assertFalse(newDisplay.hasExplicitFps());
Assert.assertEquals(new Size(1920, 1080), newDisplay.getSize());
Assert.assertEquals(0, newDisplay.getDpi());
Assert.assertEquals(0, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplayDpiOnly() {
NewDisplay newDisplay = Options.parseNewDisplay("/240");
Assert.assertFalse(newDisplay.hasExplicitSize());
Assert.assertTrue(newDisplay.hasExplicitDpi());
Assert.assertFalse(newDisplay.hasExplicitFps());
Assert.assertNull(newDisplay.getSize());
Assert.assertEquals(240, newDisplay.getDpi());
Assert.assertEquals(0, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplayFpsOnly() {
NewDisplay newDisplay = Options.parseNewDisplay("@30");
Assert.assertFalse(newDisplay.hasExplicitSize());
Assert.assertFalse(newDisplay.hasExplicitDpi());
Assert.assertTrue(newDisplay.hasExplicitFps());
Assert.assertNull(newDisplay.getSize());
Assert.assertEquals(0, newDisplay.getDpi());
Assert.assertEquals(30, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplaySizeAndDpi() {
NewDisplay newDisplay = Options.parseNewDisplay("1920x1080/240");
Assert.assertTrue(newDisplay.hasExplicitSize());
Assert.assertTrue(newDisplay.hasExplicitDpi());
Assert.assertFalse(newDisplay.hasExplicitFps());
Assert.assertEquals(new Size(1920, 1080), newDisplay.getSize());
Assert.assertEquals(240, newDisplay.getDpi());
Assert.assertEquals(0, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplaySizeAndFps() {
NewDisplay newDisplay = Options.parseNewDisplay("1920x1080@30");
Assert.assertTrue(newDisplay.hasExplicitSize());
Assert.assertFalse(newDisplay.hasExplicitDpi());
Assert.assertTrue(newDisplay.hasExplicitFps());
Assert.assertEquals(new Size(1920, 1080), newDisplay.getSize());
Assert.assertEquals(0, newDisplay.getDpi());
Assert.assertEquals(30, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplaySizeAndDpiAndFps() {
NewDisplay newDisplay = Options.parseNewDisplay("1920x1080/240@30");
Assert.assertTrue(newDisplay.hasExplicitSize());
Assert.assertTrue(newDisplay.hasExplicitDpi());
Assert.assertTrue(newDisplay.hasExplicitFps());
Assert.assertEquals(new Size(1920, 1080), newDisplay.getSize());
Assert.assertEquals(240, newDisplay.getDpi());
Assert.assertEquals(30, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplaySizeAndFpsAndDpi() {
NewDisplay newDisplay = Options.parseNewDisplay("1920x1080@30/240");
Assert.assertTrue(newDisplay.hasExplicitSize());
Assert.assertTrue(newDisplay.hasExplicitDpi());
Assert.assertTrue(newDisplay.hasExplicitFps());
Assert.assertEquals(new Size(1920, 1080), newDisplay.getSize());
Assert.assertEquals(240, newDisplay.getDpi());
Assert.assertEquals(30, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplayDpiAndFps() {
NewDisplay newDisplay = Options.parseNewDisplay("/240@30");
Assert.assertFalse(newDisplay.hasExplicitSize());
Assert.assertTrue(newDisplay.hasExplicitDpi());
Assert.assertTrue(newDisplay.hasExplicitFps());
Assert.assertNull(newDisplay.getSize());
Assert.assertEquals(240, newDisplay.getDpi());
Assert.assertEquals(30, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplayFpsAndDpi() {
NewDisplay newDisplay = Options.parseNewDisplay("@30/240");
Assert.assertFalse(newDisplay.hasExplicitSize());
Assert.assertTrue(newDisplay.hasExplicitDpi());
Assert.assertTrue(newDisplay.hasExplicitFps());
Assert.assertNull(newDisplay.getSize());
Assert.assertEquals(240, newDisplay.getDpi());
Assert.assertEquals(30, newDisplay.getFps(), 0);
}
}