From b0705e062e8e7c4e855018165a2fb768028a58f3 Mon Sep 17 00:00:00 2001 From: Roman Kiryanov Date: Fri, 2 Apr 2021 21:25:57 -0700 Subject: [PATCH] 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 --- .../server/clipboard/ClipboardService.java | 164 ++--------------- .../clipboard/EmulatorClipboardMonitor.java | 168 ++++++++++++++++++ .../java/com/android/server/clipboard/OWNERS | 1 + 3 files changed, 179 insertions(+), 154 deletions(-) create mode 100644 services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java create mode 100644 services/core/java/com/android/server/clipboard/OWNERS diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java index 2bc81cb569961..291a8f023c580 100644 --- a/services/core/java/com/android/server/clipboard/ClipboardService.java +++ b/services/core/java/com/android/server/clipboard/ClipboardService.java @@ -57,10 +57,6 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.DeviceConfig; 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.util.ArrayMap; import android.util.Slog; @@ -82,128 +78,9 @@ import com.android.server.contentcapture.ContentCaptureManagerInternal; import com.android.server.uri.UriGrantsManagerInternal; 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.List; - -// 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()); - } - } -} +import java.util.function.Consumer; /** * Implementation of the clipboard for copy and paste. @@ -235,7 +112,7 @@ public class ClipboardService extends SystemService { private final ContentCaptureManagerInternal mContentCaptureInternal; private final AutofillManagerInternal mAutofillInternal; private final IBinder mPermissionOwner; - private final HostClipboardMonitor mHostClipboardMonitor; + private final Consumer mEmulatorClipboardMonitor; private final Handler mWorkerHandler; @GuardedBy("mLock") @@ -267,24 +144,14 @@ public class ClipboardService extends SystemService { final IBinder permOwner = mUgmInternal.newUriPermissionOwner("clipboard"); mPermissionOwner = permOwner; if (IS_EMULATOR) { - mHostClipboardMonitor = new HostClipboardMonitor( - new HostClipboardMonitor.HostClipboardCallback() { - @Override - public void onHostClipboardUpdated(String contents){ - 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(); + mEmulatorClipboardMonitor = new EmulatorClipboardMonitor((clip) -> { + synchronized (mLock) { + setPrimaryClipInternalLocked(getClipboardLocked(0), clip, + android.os.Process.SYSTEM_UID, null); + } + }); } else { - mHostClipboardMonitor = null; + mEmulatorClipboardMonitor = (clip) -> {}; } updateConfig(); @@ -643,18 +510,7 @@ public class ClipboardService extends SystemService { @GuardedBy("mLock") private void setPrimaryClipInternalLocked( @Nullable ClipData clip, int uid, @Nullable String sourcePackage) { - // Push clipboard to host, if any - 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()); - } - } - } + mEmulatorClipboardMonitor.accept(clip); final int userId = UserHandle.getUserId(uid); if (clip != null) { diff --git a/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java b/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java new file mode 100644 index 0000000000000..62b701aff3981 --- /dev/null +++ b/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java @@ -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 { + 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 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) { + } + } +} diff --git a/services/core/java/com/android/server/clipboard/OWNERS b/services/core/java/com/android/server/clipboard/OWNERS new file mode 100644 index 0000000000000..5449df9080517 --- /dev/null +++ b/services/core/java/com/android/server/clipboard/OWNERS @@ -0,0 +1 @@ +per-file EmulatorClipboardMonitor.java = bohu@google.com,lfy@google.com,rkir@google.com