Synchronize device clipboard to computer
Automatically synchronize the device clipboard to the computer any time it changes. This allows seamless copy-paste from Android to the computer. Fixes #1056 <https://github.com/Genymobile/scrcpy/issues/1056#issuecomment-631363684> PR #1423 <https://github.com/Genymobile/scrcpy/pull/1423>
This commit is contained in:
parent
73e722784d
commit
acc4ef31df
5 changed files with 97 additions and 2 deletions
|
@ -482,6 +482,9 @@ both directions:
|
|||
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
|
||||
breaks non-ASCII characters).
|
||||
|
||||
Moreover, any time the Android clipboard changes, it is automatically
|
||||
synchronized to the computer clipboard.
|
||||
|
||||
#### Text injection preference
|
||||
|
||||
There are two kinds of [events][textevents] generated when typing text:
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Copyright (c) 2008, The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.content;
|
||||
|
||||
/**
|
||||
* {@hide}
|
||||
*/
|
||||
oneway interface IOnPrimaryClipChangedListener {
|
||||
void dispatchPrimaryClipChanged();
|
||||
}
|
|
@ -6,10 +6,10 @@ import com.genymobile.scrcpy.wrappers.ServiceManager;
|
|||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||
import com.genymobile.scrcpy.wrappers.WindowManager;
|
||||
|
||||
import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.InputEvent;
|
||||
|
||||
|
@ -22,10 +22,15 @@ public final class Device {
|
|||
void onRotationChanged(int rotation);
|
||||
}
|
||||
|
||||
public interface ClipboardListener {
|
||||
void onClipboardTextChanged(String text);
|
||||
}
|
||||
|
||||
private final ServiceManager serviceManager = new ServiceManager();
|
||||
|
||||
private ScreenInfo screenInfo;
|
||||
private RotationListener rotationListener;
|
||||
private ClipboardListener clipboardListener;
|
||||
|
||||
/**
|
||||
* Logical display identifier
|
||||
|
@ -66,6 +71,23 @@ public final class Device {
|
|||
}
|
||||
}, displayId);
|
||||
|
||||
if (options.getControl()) {
|
||||
// If control is enabled, synchronize Android clipboard to the computer automatically
|
||||
serviceManager.getClipboardManager().addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
|
||||
@Override
|
||||
public void dispatchPrimaryClipChanged() {
|
||||
synchronized (Device.this) {
|
||||
if (clipboardListener != null) {
|
||||
String text = getClipboardText();
|
||||
if (text != null) {
|
||||
clipboardListener.onClipboardTextChanged(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
||||
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
|
||||
}
|
||||
|
@ -138,6 +160,10 @@ public final class Device {
|
|||
this.rotationListener = rotationListener;
|
||||
}
|
||||
|
||||
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
|
||||
this.clipboardListener = clipboardListener;
|
||||
}
|
||||
|
||||
public void expandNotificationPanel() {
|
||||
serviceManager.getStatusBarManager().expandNotificationsPanel();
|
||||
}
|
||||
|
|
|
@ -53,11 +53,18 @@ public final class Server {
|
|||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps());
|
||||
|
||||
if (options.getControl()) {
|
||||
Controller controller = new Controller(device, connection);
|
||||
final Controller controller = new Controller(device, connection);
|
||||
|
||||
// asynchronous
|
||||
startController(controller);
|
||||
startDeviceMessageSender(controller.getSender());
|
||||
|
||||
device.setClipboardListener(new Device.ClipboardListener() {
|
||||
@Override
|
||||
public void onClipboardTextChanged(String text) {
|
||||
controller.getSender().pushClipboardText(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers;
|
|||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.os.Build;
|
||||
import android.os.IInterface;
|
||||
|
||||
|
@ -13,6 +14,7 @@ public class ClipboardManager {
|
|||
private final IInterface manager;
|
||||
private Method getPrimaryClipMethod;
|
||||
private Method setPrimaryClipMethod;
|
||||
private Method addPrimaryClipChangedListener;
|
||||
|
||||
public ClipboardManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
|
@ -81,4 +83,37 @@ public class ClipboardManager {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener)
|
||||
throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
|
||||
} else {
|
||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
|
||||
if (addPrimaryClipChangedListener == null) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
|
||||
} else {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
|
||||
}
|
||||
}
|
||||
return addPrimaryClipChangedListener;
|
||||
}
|
||||
|
||||
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
|
||||
try {
|
||||
Method method = getAddPrimaryClipChangedListener();
|
||||
addPrimaryClipChangedListener(method, manager, listener);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue