Merge "Separate the emulator specific part of ClipboardService.java"
This commit is contained in:
@@ -52,10 +52,6 @@ import android.os.SystemProperties;
|
|||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
import android.os.UserManager;
|
import android.os.UserManager;
|
||||||
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.Slog;
|
import android.util.Slog;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
@@ -67,128 +63,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.
|
||||||
@@ -214,8 +91,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 HostClipboardMonitor mHostClipboardMonitor = null;
|
private final Consumer<ClipData> mEmulatorClipboardMonitor;
|
||||||
private Thread mHostMonitorThread = null;
|
|
||||||
|
|
||||||
private final SparseArray<PerUserClipboard> mClipboards = new SparseArray<>();
|
private final SparseArray<PerUserClipboard> mClipboards = new SparseArray<>();
|
||||||
|
|
||||||
@@ -237,22 +113,13 @@ 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 (this) {
|
||||||
@Override
|
setPrimaryClipInternal(getClipboard(0), clip, android.os.Process.SYSTEM_UID);
|
||||||
public void onHostClipboardUpdated(String contents){
|
}
|
||||||
ClipData clip =
|
});
|
||||||
new ClipData("host clipboard",
|
} else {
|
||||||
new String[]{"text/plain"},
|
mEmulatorClipboardMonitor = (clip) -> {};
|
||||||
new ClipData.Item(contents));
|
|
||||||
synchronized(mClipboards) {
|
|
||||||
setPrimaryClipInternal(getClipboard(0), clip,
|
|
||||||
android.os.Process.SYSTEM_UID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
mHostMonitorThread = new Thread(mHostClipboardMonitor);
|
|
||||||
mHostMonitorThread.start();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,18 +414,7 @@ public class ClipboardService extends SystemService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setPrimaryClipInternal(@Nullable ClipData clip, int uid) {
|
void setPrimaryClipInternal(@Nullable ClipData clip, int uid) {
|
||||||
// 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update this user
|
// Update this user
|
||||||
final int userId = UserHandle.getUserId(uid);
|
final int userId = UserHandle.getUserId(uid);
|
||||||
|
|||||||
@@ -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