Compare commits

...

6 Commits

Author SHA1 Message Date
104195fc3b Add shortcut to reset video capture/encoding
Reset video capture/encoding on MOD+Shift+r.

Like on device rotation, this starts a new encoding session which
produces a video stream starting by a key frame.

PR #5432 <https://github.com/Genymobile/scrcpy/pull/5432>
2024-11-03 19:31:02 +01:00
9958302e6f Interrupt MediaCodec blocking call on reset
When the MediaCodec input is a Surface, no EOS (end-of-stream) will
never occur automatically: it may only be triggered manually by
MediaCodec.signalEndOfInputStream().

Use this signal to interrupt the blocking call to dequeueOutputBuffer()
immediately on reset, without waiting for the next frame to be dequeued.

PR #5432 <https://github.com/Genymobile/scrcpy/pull/5432>
2024-11-03 19:27:11 +01:00
69b836930a Handle capture reset via listener
When the capture source becomes "invalid" (because the display size
changes for example), a reset request is performed to restart the
encoder.

The reset state was stored in SurfaceCapture. The capture implementation
set the flag, and the encoder consumed it.

However, this mechanism did not allow a reset request to _interrupt_ the
encoder, which may be waiting on a blocking call (until a new frame is
produced).

To be able to interrupt the encoder, a reset request must not only set a
flag, but run a callback provided by the encoder. For that purpose,
introduce the CaptureListener interface, which is notified by the
SurfaceCapture implementation whenever the capture is invalidated.

For now, the listener implementation just set a flag as before, so the
behavior is unchanged. It lays the groundwork for the next commits.

PR #5432 <https://github.com/Genymobile/scrcpy/pull/5432>
2024-11-03 19:26:55 +01:00
790ea5e58c Check screen on for current displayId
Since Android 14, the "screen on" state can be checked per-display.

Refs <956f4084df%5E!/#F17>
PR #5442 <https://github.com/Genymobile/scrcpy/pull/5442>
2024-11-03 19:10:44 +01:00
1270997f6b Remove useless assignment
The local variable virtualDisplayId was already initialized to the exact
same value.
2024-11-03 19:02:57 +01:00
c905fbba8d Fix indentation 2024-11-03 19:02:37 +01:00
20 changed files with 211 additions and 67 deletions

View File

@ -671,6 +671,10 @@ Pause or re-pause display
.B MOD+Shift+z
Unpause display
.TP
.B MOD+Shift+r
Reset video capture/encoding
.TP
.B MOD+g
Resize window to 1:1 (pixel\-perfect)

View File

@ -1022,6 +1022,10 @@ static const struct sc_shortcut shortcuts[] = {
.shortcuts = { "MOD+Shift+z" },
.text = "Unpause display",
},
{
.shortcuts = { "MOD+Shift+r" },
.text = "Reset video capture/encoding",
},
{
.shortcuts = { "MOD+g" },
.text = "Resize window to 1:1 (pixel-perfect)",

View File

@ -181,6 +181,7 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
// no additional data
return 1;
default:
@ -304,6 +305,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
case SC_CONTROL_MSG_TYPE_START_APP:
LOG_CMSG("start app \"%s\"", msg->start_app.name);
break;
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
LOG_CMSG("reset video");
break;
default:
LOG_CMSG("unknown type: %u", (unsigned) msg->type);
break;

View File

@ -42,6 +42,7 @@ enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_UHID_DESTROY,
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
SC_CONTROL_MSG_TYPE_START_APP,
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
};
enum sc_copy_key {

View File

@ -284,6 +284,18 @@ open_hard_keyboard_settings(struct sc_input_manager *im) {
}
}
static void
reset_video(struct sc_input_manager *im) {
assert(im->controller);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO;
if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request reset video");
}
}
static void
apply_orientation_transform(struct sc_input_manager *im,
enum sc_orientation transform) {
@ -521,8 +533,12 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
return;
case SDLK_r:
if (control && !shift && !repeat && down && !paused) {
rotate_device(im);
if (control && !repeat && down && !paused) {
if (shift) {
reset_video(im);
} else {
rotate_device(im);
}
}
return;
case SDLK_k:

View File

@ -407,6 +407,21 @@ static void test_serialize_open_hard_keyboard(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_reset_video(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO,
};
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
@ -429,5 +444,6 @@ int main(int argc, char *argv[]) {
test_serialize_uhid_input();
test_serialize_uhid_destroy();
test_serialize_open_hard_keyboard();
test_serialize_reset_video();
return 0;
}

View File

@ -30,6 +30,7 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Flip display vertically | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(up)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(down)_
| Pause or re-pause display | <kbd>MOD</kbd>+<kbd>z</kbd>
| Unpause display | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>z</kbd>
| Reset video capture/encoding | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>r</kbd>
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_

View File

@ -137,7 +137,7 @@ public final class CleanUp {
}
}
if (Device.isScreenOn() && displayId != Device.DISPLAY_ID_NONE) {
if (displayId != Device.DISPLAY_ID_NONE && Device.isScreenOn(displayId)) {
if (powerOffScreen) {
Ln.i("Power off screen");
Device.powerOffScreen(displayId);

View File

@ -225,6 +225,10 @@ public final class Server {
SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
asyncProcessors.add(surfaceEncoder);
if (controller != null) {
controller.setSurfaceCapture(surfaceCapture);
}
}
Completion completion = new Completion(asyncProcessors.size());

View File

@ -24,6 +24,7 @@ public final class ControlMessage {
public static final int TYPE_UHID_DESTROY = 14;
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
public static final int TYPE_START_APP = 16;
public static final int TYPE_RESET_VIDEO = 17;
public static final long SEQUENCE_INVALID = 0;

View File

@ -46,6 +46,7 @@ public class ControlMessageReader {
case ControlMessage.TYPE_COLLAPSE_PANELS:
case ControlMessage.TYPE_ROTATE_DEVICE:
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case ControlMessage.TYPE_RESET_VIDEO:
return ControlMessage.createEmpty(type);
case ControlMessage.TYPE_UHID_CREATE:
return parseUhidCreate();

View File

@ -9,6 +9,7 @@ import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.VirtualDisplayListener;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.InputManager;
@ -93,6 +94,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private boolean keepDisplayPowerOff;
// Used for resetting video encoding on RESET_VIDEO message
private SurfaceCapture surfaceCapture;
public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) {
this.displayId = displayId;
this.controlChannel = controlChannel;
@ -143,6 +147,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
}
}
public void setSurfaceCapture(SurfaceCapture surfaceCapture) {
this.surfaceCapture = surfaceCapture;
}
private UhidManager getUhidManager() {
if (uhidManager == null) {
uhidManager = new UhidManager(sender);
@ -166,7 +174,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private void control() throws IOException {
// on start, power on the device
if (powerOn && displayId == 0 && !Device.isScreenOn()) {
if (powerOn && displayId == 0 && !Device.isScreenOn(displayId)) {
Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
// dirty hack
@ -293,6 +301,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
case ControlMessage.TYPE_START_APP:
startAppAsync(msg.getText());
break;
case ControlMessage.TYPE_RESET_VIDEO:
resetVideo();
break;
default:
// do nothing
}
@ -490,7 +501,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
}
private boolean pressBackOrTurnScreenOn(int action) {
if (Device.isScreenOn()) {
if (displayId == Device.DISPLAY_ID_NONE || Device.isScreenOn(displayId)) {
return injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC);
}
@ -680,4 +691,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
}
}
}
private void resetVideo() {
if (surfaceCapture != null) {
Ln.i("Video capture reset");
surfaceCapture.requestInvalidate();
}
}
}

View File

@ -82,8 +82,9 @@ public final class Device {
&& injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode);
}
public static boolean isScreenOn() {
return ServiceManager.getPowerManager().isScreenOn();
public static boolean isScreenOn(int displayId) {
assert displayId != DISPLAY_ID_NONE;
return ServiceManager.getPowerManager().isScreenOn(displayId);
}
public static void expandNotificationPanel() {
@ -181,7 +182,7 @@ public final class Device {
public static boolean powerOffScreen(int displayId) {
assert displayId != DISPLAY_ID_NONE;
if (!isScreenOn()) {
if (!isScreenOn(displayId)) {
return true;
}
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);

View File

@ -68,7 +68,7 @@ public class CameraCapture extends SurfaceCapture {
}
@Override
public void init() throws IOException {
protected void init() throws IOException {
cameraThread = new HandlerThread("camera");
cameraThread.start();
cameraHandler = new Handler(cameraThread.getLooper());
@ -256,7 +256,7 @@ public class CameraCapture extends SurfaceCapture {
public void onDisconnected(CameraDevice camera) {
Ln.w("Camera disconnected");
disconnected.set(true);
requestReset();
invalidate();
}
@Override
@ -355,4 +355,9 @@ public class CameraCapture extends SurfaceCapture {
public boolean isClosed() {
return disconnected.get();
}
@Override
public void requestInvalidate() {
// do nothing (the user could not request a reset anyway for now, since there is no controller for camera mirroring)
}
}

View File

@ -0,0 +1,33 @@
package com.genymobile.scrcpy.video;
import android.media.MediaCodec;
import java.util.concurrent.atomic.AtomicBoolean;
public class CaptureReset implements SurfaceCapture.CaptureListener {
private final AtomicBoolean reset = new AtomicBoolean();
// Current instance of MediaCodec to "interrupt" on reset
private MediaCodec runningMediaCodec;
public boolean consumeReset() {
return reset.getAndSet(false);
}
public synchronized void reset() {
reset.set(true);
if (runningMediaCodec != null) {
runningMediaCodec.signalEndOfInputStream();
}
}
public synchronized void setRunningMediaCodec(MediaCodec runningMediaCodec) {
this.runningMediaCodec = runningMediaCodec;
}
@Override
public void onInvalidated() {
reset();
}
}

View File

@ -14,6 +14,8 @@ import android.hardware.display.VirtualDisplay;
import android.os.Build;
import android.view.Surface;
import java.io.IOException;
public class NewDisplayCapture extends SurfaceCapture {
// Internal fields copied from android.hardware.display.DisplayManager
@ -46,7 +48,7 @@ public class NewDisplayCapture extends SurfaceCapture {
}
@Override
public void init() {
protected void init() {
size = newDisplay.getSize();
dpi = newDisplay.getDpi();
if (size == null || dpi == 0) {
@ -72,13 +74,8 @@ public class NewDisplayCapture extends SurfaceCapture {
}
}
@Override
public void start(Surface surface) {
if (virtualDisplay != null) {
virtualDisplay.release();
virtualDisplay = null;
}
public void startNew(Surface surface) {
int virtualDisplayId;
try {
int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
@ -93,8 +90,8 @@ public class NewDisplayCapture extends SurfaceCapture {
| VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED
| VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED;
if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS
| VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS
| VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
}
}
virtualDisplay = ServiceManager.getDisplayManager()
@ -107,13 +104,21 @@ public class NewDisplayCapture extends SurfaceCapture {
}
if (vdListener != null) {
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
Rect contentRect = new Rect(0, 0, size.getWidth(), size.getHeight());
PositionMapper positionMapper = new PositionMapper(size, contentRect, 0);
vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper);
}
}
@Override
public void start(Surface surface) throws IOException {
if (virtualDisplay == null) {
startNew(surface);
} else {
virtualDisplay.setSurface(surface);
}
}
@Override
public void release() {
if (virtualDisplay != null) {
@ -143,4 +148,9 @@ public class NewDisplayCapture extends SurfaceCapture {
int num = size.getMax();
return initialDpi * num / den;
}
@Override
public void requestInvalidate() {
invalidate();
}
}

View File

@ -85,7 +85,7 @@ public class ScreenCapture extends SurfaceCapture {
Ln.v("ScreenCapture: requestReset(): " + getSessionDisplaySize() + " -> (unknown)");
}
setSessionDisplaySize(null);
requestReset();
invalidate();
} else {
Size size = di.getSize();
@ -102,7 +102,7 @@ public class ScreenCapture extends SurfaceCapture {
// Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare()
// considers that the current size is the requested size (to avoid a duplicate requestReset())
setSessionDisplaySize(size);
requestReset();
invalidate();
} else if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: Size not changed (" + size + "): do not requestReset()");
}
@ -246,7 +246,7 @@ public class ScreenCapture extends SurfaceCapture {
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")");
}
requestReset();
invalidate();
}
};
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
@ -272,7 +272,7 @@ public class ScreenCapture extends SurfaceCapture {
// Ignore events related to other display ids
return;
}
requestReset();
invalidate();
}
};
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
@ -291,4 +291,9 @@ public class ScreenCapture extends SurfaceCapture {
}
}
}
@Override
public void requestInvalidate() {
invalidate();
}
}

View File

@ -6,36 +6,37 @@ import com.genymobile.scrcpy.device.Size;
import android.view.Surface;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A video source which can be rendered on a Surface for encoding.
*/
public abstract class SurfaceCapture {
private final AtomicBoolean resetCapture = new AtomicBoolean();
/**
* Request the encoding session to be restarted, for example if the capture implementation detects that the video source size has changed (on
* device rotation for example).
*/
protected void requestReset() {
resetCapture.set(true);
public interface CaptureListener {
void onInvalidated();
}
private CaptureListener listener;
/**
* Consume the reset request (intended to be called by the encoder).
*
* @return {@code true} if a reset request was pending, {@code false} otherwise.
* Notify the listener that the capture has been invalidated (for example, because its size changed).
*/
public boolean consumeReset() {
return resetCapture.getAndSet(false);
protected void invalidate() {
listener.onInvalidated();
}
/**
* Called once before the first capture starts.
*/
public abstract void init() throws ConfigurationException, IOException;
public final void init(CaptureListener listener) throws ConfigurationException, IOException {
this.listener = listener;
init();
}
/**
* Called once before the first capture starts.
*/
protected abstract void init() throws ConfigurationException, IOException;
/**
* Called after the last capture ends (if and only if {@link #init()} has been called).
@ -78,4 +79,11 @@ public abstract class SurfaceCapture {
public boolean isClosed() {
return false;
}
/**
* Manually request to invalidate (typically a user request).
* <p>
* The capture implementation is free to ignore the request and do nothing.
*/
public abstract void requestInvalidate();
}

View File

@ -49,6 +49,8 @@ public class SurfaceEncoder implements AsyncProcessor {
private Thread thread;
private final AtomicBoolean stopped = new AtomicBoolean();
private final CaptureReset reset = new CaptureReset();
public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, float maxFps, List<CodecOption> codecOptions,
String encoderName, boolean downsizeOnError) {
this.capture = capture;
@ -65,14 +67,14 @@ public class SurfaceEncoder implements AsyncProcessor {
MediaCodec mediaCodec = createMediaCodec(codec, encoderName);
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
capture.init();
capture.init(reset);
try {
boolean alive;
boolean headerWritten = false;
do {
capture.consumeReset(); // If a capture reset was requested, it is implicitly fulfilled
reset.consumeReset(); // If a capture reset was requested, it is implicitly fulfilled
capture.prepare();
Size size = capture.getSize();
if (!headerWritten) {
@ -92,7 +94,21 @@ public class SurfaceEncoder implements AsyncProcessor {
mediaCodec.start();
alive = encode(mediaCodec, streamer);
// Set the MediaCodec instance to "interrupt" (by signaling an EOS) on reset
reset.setRunningMediaCodec(mediaCodec);
if (stopped.get()) {
alive = false;
} else {
boolean resetRequested = reset.consumeReset();
if (!resetRequested) {
// If a reset is requested during encode(), it will interrupt the encoding by an EOS
encode(mediaCodec, streamer);
}
// The capture might have been closed internally (for example if the camera is disconnected)
alive = !stopped.get() && !capture.isClosed();
}
// do not call stop() on exception, it would trigger an IllegalStateException
mediaCodec.stop();
} catch (IllegalStateException | IllegalArgumentException e) {
@ -103,6 +119,7 @@ public class SurfaceEncoder implements AsyncProcessor {
Ln.i("Retrying...");
alive = true;
} finally {
reset.setRunningMediaCodec(null);
mediaCodec.reset();
if (surface != null) {
surface.release();
@ -163,25 +180,16 @@ public class SurfaceEncoder implements AsyncProcessor {
return 0;
}
private boolean encode(MediaCodec codec, Streamer streamer) throws IOException {
boolean eof = false;
boolean alive = true;
private void encode(MediaCodec codec, Streamer streamer) throws IOException {
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!capture.consumeReset() && !eof) {
if (stopped.get()) {
alive = false;
break;
}
boolean eos;
do {
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
try {
if (capture.consumeReset()) {
// must restart encoding with new size
break;
}
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
if (outputBufferId >= 0) {
eos = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
// On EOS, there might be data or not, depending on bufferInfo.size
if (outputBufferId >= 0 && bufferInfo.size > 0) {
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
@ -198,14 +206,7 @@ public class SurfaceEncoder implements AsyncProcessor {
codec.releaseOutputBuffer(outputBufferId, false);
}
}
}
if (capture.isClosed()) {
// The capture might have been closed internally (for example if the camera is disconnected)
alive = false;
}
return !eof && alive;
} while (!eos);
}
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
@ -298,6 +299,7 @@ public class SurfaceEncoder implements AsyncProcessor {
public void stop() {
if (thread != null) {
stopped.set(true);
reset.reset();
}
}

View File

@ -1,7 +1,9 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln;
import android.os.Build;
import android.os.IInterface;
import java.lang.reflect.Method;
@ -21,14 +23,22 @@ public final class PowerManager {
private Method getIsScreenOnMethod() throws NoSuchMethodException {
if (isScreenOnMethod == null) {
isScreenOnMethod = manager.getClass().getMethod("isInteractive");
if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
isScreenOnMethod = manager.getClass().getMethod("isDisplayInteractive", int.class);
} else {
isScreenOnMethod = manager.getClass().getMethod("isInteractive");
}
}
return isScreenOnMethod;
}
public boolean isScreenOn() {
public boolean isScreenOn(int displayId) {
try {
Method method = getIsScreenOnMethod();
if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
return (boolean) method.invoke(manager, displayId);
}
return (boolean) method.invoke(manager);
} catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e);