Separate the emulator specific part of ClipboardService.java
This way the Android Studio Emulator will be the owner of the emulator specific code and the framework will not see the implemetattion details. Bug: 182436079 Test: check if emulator exchanges the clipboard with the host Change-Id: Icdb25d51437ba0f8e98a2750b0b705c15755f106
This commit is contained in:
@@ -57,10 +57,6 @@ import android.os.UserHandle;
|
|||||||
import android.os.UserManager;
|
import android.os.UserManager;
|
||||||
import android.provider.DeviceConfig;
|
import android.provider.DeviceConfig;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.system.ErrnoException;
|
|
||||||
import android.system.Os;
|
|
||||||
import android.system.OsConstants;
|
|
||||||
import android.system.VmSocketAddress;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.ArrayMap;
|
import android.util.ArrayMap;
|
||||||
import android.util.Slog;
|
import android.util.Slog;
|
||||||
@@ -82,128 +78,9 @@ import com.android.server.contentcapture.ContentCaptureManagerInternal;
|
|||||||
import com.android.server.uri.UriGrantsManagerInternal;
|
import com.android.server.uri.UriGrantsManagerInternal;
|
||||||
import com.android.server.wm.WindowManagerInternal;
|
import com.android.server.wm.WindowManagerInternal;
|
||||||
|
|
||||||
import java.io.FileDescriptor;
|
|
||||||
import java.io.InterruptedIOException;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
// The following class is Android Emulator specific. It is used to read and
|
|
||||||
// write contents of the host system's clipboard.
|
|
||||||
class HostClipboardMonitor implements Runnable {
|
|
||||||
public interface HostClipboardCallback {
|
|
||||||
void onHostClipboardUpdated(String contents);
|
|
||||||
}
|
|
||||||
|
|
||||||
private FileDescriptor mPipe = null;
|
|
||||||
private HostClipboardCallback mHostClipboardCallback;
|
|
||||||
private static final String PIPE_NAME = "pipe:clipboard";
|
|
||||||
private static final int HOST_PORT = 5000;
|
|
||||||
|
|
||||||
private static byte[] createOpenHandshake() {
|
|
||||||
// String.getBytes doesn't include the null terminator,
|
|
||||||
// but the QEMU pipe device requires the pipe service name
|
|
||||||
// to be null-terminated.
|
|
||||||
|
|
||||||
final byte[] bits = Arrays.copyOf(PIPE_NAME.getBytes(), PIPE_NAME.length() + 1);
|
|
||||||
bits[PIPE_NAME.length()] = 0;
|
|
||||||
return bits;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean openPipe() {
|
|
||||||
try {
|
|
||||||
final FileDescriptor fd = Os.socket(OsConstants.AF_VSOCK, OsConstants.SOCK_STREAM, 0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Os.connect(fd, new VmSocketAddress(HOST_PORT, OsConstants.VMADDR_CID_HOST));
|
|
||||||
|
|
||||||
final byte[] handshake = createOpenHandshake();
|
|
||||||
Os.write(fd, handshake, 0, handshake.length);
|
|
||||||
mPipe = fd;
|
|
||||||
return true;
|
|
||||||
} catch (ErrnoException | SocketException | InterruptedIOException e) {
|
|
||||||
Os.close(fd);
|
|
||||||
}
|
|
||||||
} catch (ErrnoException e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void closePipe() {
|
|
||||||
try {
|
|
||||||
final FileDescriptor fd = mPipe;
|
|
||||||
mPipe = null;
|
|
||||||
if (fd != null) {
|
|
||||||
Os.close(fd);
|
|
||||||
}
|
|
||||||
} catch (ErrnoException ignore) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] receiveMessage() throws ErrnoException, InterruptedIOException {
|
|
||||||
final byte[] lengthBits = new byte[4];
|
|
||||||
Os.read(mPipe, lengthBits, 0, lengthBits.length);
|
|
||||||
|
|
||||||
final ByteBuffer bb = ByteBuffer.wrap(lengthBits);
|
|
||||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
final int msgLen = bb.getInt();
|
|
||||||
|
|
||||||
final byte[] msg = new byte[msgLen];
|
|
||||||
Os.read(mPipe, msg, 0, msg.length);
|
|
||||||
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendMessage(byte[] msg) throws ErrnoException, InterruptedIOException {
|
|
||||||
final byte[] lengthBits = new byte[4];
|
|
||||||
final ByteBuffer bb = ByteBuffer.wrap(lengthBits);
|
|
||||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
bb.putInt(msg.length);
|
|
||||||
|
|
||||||
Os.write(mPipe, lengthBits, 0, lengthBits.length);
|
|
||||||
Os.write(mPipe, msg, 0, msg.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HostClipboardMonitor(HostClipboardCallback cb) {
|
|
||||||
mHostClipboardCallback = cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
while (!Thread.interrupted()) {
|
|
||||||
try {
|
|
||||||
// There's no guarantee that QEMU pipes will be ready at the moment
|
|
||||||
// this method is invoked. We simply try to get the pipe open and
|
|
||||||
// retry on failure indefinitely.
|
|
||||||
while ((mPipe == null) && !openPipe()) {
|
|
||||||
Thread.sleep(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
final byte[] receivedData = receiveMessage();
|
|
||||||
mHostClipboardCallback.onHostClipboardUpdated(
|
|
||||||
new String(receivedData));
|
|
||||||
} catch (ErrnoException | InterruptedIOException e) {
|
|
||||||
closePipe();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHostClipboard(String content) {
|
|
||||||
try {
|
|
||||||
if (mPipe != null) {
|
|
||||||
sendMessage(content.getBytes());
|
|
||||||
}
|
|
||||||
} catch (ErrnoException | InterruptedIOException e) {
|
|
||||||
Slog.e("HostClipboardMonitor",
|
|
||||||
"Failed to set host clipboard " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of the clipboard for copy and paste.
|
* Implementation of the clipboard for copy and paste.
|
||||||
@@ -235,7 +112,7 @@ public class ClipboardService extends SystemService {
|
|||||||
private final ContentCaptureManagerInternal mContentCaptureInternal;
|
private final ContentCaptureManagerInternal mContentCaptureInternal;
|
||||||
private final AutofillManagerInternal mAutofillInternal;
|
private final AutofillManagerInternal mAutofillInternal;
|
||||||
private final IBinder mPermissionOwner;
|
private final IBinder mPermissionOwner;
|
||||||
private final HostClipboardMonitor mHostClipboardMonitor;
|
private final Consumer<ClipData> mEmulatorClipboardMonitor;
|
||||||
private final Handler mWorkerHandler;
|
private final Handler mWorkerHandler;
|
||||||
|
|
||||||
@GuardedBy("mLock")
|
@GuardedBy("mLock")
|
||||||
@@ -267,24 +144,14 @@ public class ClipboardService extends SystemService {
|
|||||||
final IBinder permOwner = mUgmInternal.newUriPermissionOwner("clipboard");
|
final IBinder permOwner = mUgmInternal.newUriPermissionOwner("clipboard");
|
||||||
mPermissionOwner = permOwner;
|
mPermissionOwner = permOwner;
|
||||||
if (IS_EMULATOR) {
|
if (IS_EMULATOR) {
|
||||||
mHostClipboardMonitor = new HostClipboardMonitor(
|
mEmulatorClipboardMonitor = new EmulatorClipboardMonitor((clip) -> {
|
||||||
new HostClipboardMonitor.HostClipboardCallback() {
|
synchronized (mLock) {
|
||||||
@Override
|
setPrimaryClipInternalLocked(getClipboardLocked(0), clip,
|
||||||
public void onHostClipboardUpdated(String contents){
|
android.os.Process.SYSTEM_UID, null);
|
||||||
ClipData clip =
|
}
|
||||||
new ClipData("host clipboard",
|
});
|
||||||
new String[]{"text/plain"},
|
|
||||||
new ClipData.Item(contents));
|
|
||||||
synchronized (mLock) {
|
|
||||||
setPrimaryClipInternalLocked(getClipboardLocked(0), clip,
|
|
||||||
android.os.Process.SYSTEM_UID, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Thread hostMonitorThread = new Thread(mHostClipboardMonitor);
|
|
||||||
hostMonitorThread.start();
|
|
||||||
} else {
|
} else {
|
||||||
mHostClipboardMonitor = null;
|
mEmulatorClipboardMonitor = (clip) -> {};
|
||||||
}
|
}
|
||||||
|
|
||||||
updateConfig();
|
updateConfig();
|
||||||
@@ -643,18 +510,7 @@ public class ClipboardService extends SystemService {
|
|||||||
@GuardedBy("mLock")
|
@GuardedBy("mLock")
|
||||||
private void setPrimaryClipInternalLocked(
|
private void setPrimaryClipInternalLocked(
|
||||||
@Nullable ClipData clip, int uid, @Nullable String sourcePackage) {
|
@Nullable ClipData clip, int uid, @Nullable String sourcePackage) {
|
||||||
// Push clipboard to host, if any
|
mEmulatorClipboardMonitor.accept(clip);
|
||||||
if (mHostClipboardMonitor != null) {
|
|
||||||
if (clip == null) {
|
|
||||||
// Someone really wants the clipboard cleared, so push empty
|
|
||||||
mHostClipboardMonitor.setHostClipboard("");
|
|
||||||
} else if (clip.getItemCount() > 0) {
|
|
||||||
final CharSequence text = clip.getItemAt(0).getText();
|
|
||||||
if (text != null) {
|
|
||||||
mHostClipboardMonitor.setHostClipboard(text.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final int userId = UserHandle.getUserId(uid);
|
final int userId = UserHandle.getUserId(uid);
|
||||||
if (clip != null) {
|
if (clip != null) {
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 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 com.android.server.clipboard;
|
||||||
|
|
||||||
|
import android.annotation.Nullable;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.system.ErrnoException;
|
||||||
|
import android.system.Os;
|
||||||
|
import android.system.OsConstants;
|
||||||
|
import android.system.VmSocketAddress;
|
||||||
|
import android.util.Slog;
|
||||||
|
|
||||||
|
import java.io.FileDescriptor;
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
// The following class is Android Emulator specific. It is used to read and
|
||||||
|
// write contents of the host system's clipboard.
|
||||||
|
class EmulatorClipboardMonitor implements Consumer<ClipData> {
|
||||||
|
private static final String TAG = "EmulatorClipboardMonitor";
|
||||||
|
private static final String PIPE_NAME = "pipe:clipboard";
|
||||||
|
private static final int HOST_PORT = 5000;
|
||||||
|
private final Thread mHostMonitorThread;
|
||||||
|
private FileDescriptor mPipe = null;
|
||||||
|
|
||||||
|
private static byte[] createOpenHandshake() {
|
||||||
|
// String.getBytes doesn't include the null terminator,
|
||||||
|
// but the QEMU pipe device requires the pipe service name
|
||||||
|
// to be null-terminated.
|
||||||
|
|
||||||
|
final byte[] bits = Arrays.copyOf(PIPE_NAME.getBytes(), PIPE_NAME.length() + 1);
|
||||||
|
bits[PIPE_NAME.length()] = 0;
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPipeOpened() {
|
||||||
|
return mPipe != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized boolean openPipe() {
|
||||||
|
if (mPipe != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final FileDescriptor fd = Os.socket(OsConstants.AF_VSOCK, OsConstants.SOCK_STREAM, 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Os.connect(fd, new VmSocketAddress(HOST_PORT, OsConstants.VMADDR_CID_HOST));
|
||||||
|
|
||||||
|
final byte[] handshake = createOpenHandshake();
|
||||||
|
Os.write(fd, handshake, 0, handshake.length);
|
||||||
|
mPipe = fd;
|
||||||
|
return true;
|
||||||
|
} catch (ErrnoException | SocketException | InterruptedIOException e) {
|
||||||
|
Os.close(fd);
|
||||||
|
}
|
||||||
|
} catch (ErrnoException e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void closePipe() {
|
||||||
|
try {
|
||||||
|
final FileDescriptor fd = mPipe;
|
||||||
|
mPipe = null;
|
||||||
|
if (fd != null) {
|
||||||
|
Os.close(fd);
|
||||||
|
}
|
||||||
|
} catch (ErrnoException ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] receiveMessage() throws ErrnoException, InterruptedIOException {
|
||||||
|
final byte[] lengthBits = new byte[4];
|
||||||
|
Os.read(mPipe, lengthBits, 0, lengthBits.length);
|
||||||
|
|
||||||
|
final ByteBuffer bb = ByteBuffer.wrap(lengthBits);
|
||||||
|
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
final int msgLen = bb.getInt();
|
||||||
|
|
||||||
|
final byte[] msg = new byte[msgLen];
|
||||||
|
Os.read(mPipe, msg, 0, msg.length);
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendMessage(final byte[] msg) throws ErrnoException, InterruptedIOException {
|
||||||
|
final byte[] lengthBits = new byte[4];
|
||||||
|
final ByteBuffer bb = ByteBuffer.wrap(lengthBits);
|
||||||
|
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
bb.putInt(msg.length);
|
||||||
|
|
||||||
|
Os.write(mPipe, lengthBits, 0, lengthBits.length);
|
||||||
|
Os.write(mPipe, msg, 0, msg.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
EmulatorClipboardMonitor(final Consumer<ClipData> setAndroidClipboard) {
|
||||||
|
this.mHostMonitorThread = new Thread(() -> {
|
||||||
|
while (!Thread.interrupted()) {
|
||||||
|
try {
|
||||||
|
// There's no guarantee that QEMU pipes will be ready at the moment
|
||||||
|
// this method is invoked. We simply try to get the pipe open and
|
||||||
|
// retry on failure indefinitely.
|
||||||
|
while (!openPipe()) {
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] receivedData = receiveMessage();
|
||||||
|
|
||||||
|
final String str = new String(receivedData);
|
||||||
|
final ClipData clip = new ClipData("host clipboard",
|
||||||
|
new String[]{"text/plain"},
|
||||||
|
new ClipData.Item(str));
|
||||||
|
|
||||||
|
setAndroidClipboard.accept(clip);
|
||||||
|
} catch (ErrnoException | InterruptedIOException e) {
|
||||||
|
closePipe();
|
||||||
|
} catch (InterruptedException | IllegalArgumentException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mHostMonitorThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(final @Nullable ClipData clip) {
|
||||||
|
if (clip == null) {
|
||||||
|
setHostClipboardImpl("");
|
||||||
|
} else if (clip.getItemCount() > 0) {
|
||||||
|
final CharSequence text = clip.getItemAt(0).getText();
|
||||||
|
if (text != null) {
|
||||||
|
setHostClipboardImpl(text.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setHostClipboardImpl(final String value) {
|
||||||
|
try {
|
||||||
|
if (isPipeOpened()) {
|
||||||
|
sendMessage(value.getBytes());
|
||||||
|
}
|
||||||
|
} catch (ErrnoException | InterruptedIOException e) {
|
||||||
|
Slog.e(TAG, "Failed to set host clipboard " + e.getMessage());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
services/core/java/com/android/server/clipboard/OWNERS
Normal file
1
services/core/java/com/android/server/clipboard/OWNERS
Normal file
@@ -0,0 +1 @@
|
|||||||
|
per-file EmulatorClipboardMonitor.java = bohu@google.com,lfy@google.com,rkir@google.com
|
||||||
Reference in New Issue
Block a user