Skip to content

Commit

Permalink
Synchronize device clipboard to computer
Browse files Browse the repository at this point in the history
Automatically synchronize the device clipboard to the computer any time
it changes.

This allows seamless copy-paste from Android to the computer.

Fixes #1056 <#1056 (comment)>
PR #1423 <#1423>
  • Loading branch information
rom1v committed May 24, 2020
1 parent 73e7227 commit acc4ef3
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 2 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
28 changes: 27 additions & 1 deletion server/src/main/java/com/genymobile/scrcpy/Device.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
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;

Expand All @@ -22,10 +22,15 @@ public interface RotationListener {
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
Expand Down Expand Up @@ -66,6 +71,23 @@ public void onRotationChanged(int rotation) {
}
}, 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");
}
Expand Down Expand Up @@ -138,6 +160,10 @@ public synchronized void setRotationListener(RotationListener rotationListener)
this.rotationListener = rotationListener;
}

public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
this.clipboardListener = clipboardListener;
}

public void expandNotificationPanel() {
serviceManager.getStatusBarManager().expandNotificationsPanel();
}
Expand Down
9 changes: 8 additions & 1 deletion server/src/main/java/com/genymobile/scrcpy/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,18 @@ private static void scrcpy(Options options) throws IOException {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.genymobile.scrcpy.Ln;

import android.content.ClipData;
import android.content.IOnPrimaryClipChangedListener;
import android.os.Build;
import android.os.IInterface;

Expand All @@ -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;
Expand Down Expand Up @@ -81,4 +83,37 @@ public boolean setText(CharSequence text) {
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;
}
}
}

0 comments on commit acc4ef3

Please sign in to comment.