| /* |
| * 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.os.PersistableBundle; |
| import android.os.SystemProperties; |
| import android.system.ErrnoException; |
| import android.system.Os; |
| import android.system.OsConstants; |
| import android.system.VmSocketAddress; |
| import android.util.Slog; |
| |
| import java.io.EOFException; |
| import java.io.FileDescriptor; |
| import java.io.InterruptedIOException; |
| import java.net.ProtocolException; |
| 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 static final boolean LOG_CLIBOARD_ACCESS = |
| SystemProperties.getBoolean("ro.boot.qemu.log_clipboard_access", false); |
| private static final int MAX_CLIPBOARD_BYTES = 128 << 20; |
| |
| private FileDescriptor mPipe = null; |
| private final Thread mHostMonitorThread; |
| |
| 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 synchronized FileDescriptor getPipeFD() { |
| return mPipe; |
| } |
| |
| private synchronized void setPipeFD(final FileDescriptor fd) { |
| mPipe = fd; |
| } |
| |
| private static FileDescriptor openPipeImpl() { |
| 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(); |
| writeFully(fd, handshake, 0, handshake.length); |
| return fd; |
| } catch (ErrnoException | SocketException | InterruptedIOException e) { |
| Os.close(fd); |
| } |
| } catch (ErrnoException e) { |
| } |
| |
| return null; |
| } |
| |
| private static FileDescriptor openPipe() throws InterruptedException { |
| FileDescriptor fd = openPipeImpl(); |
| |
| // 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 (fd == null) { |
| Thread.sleep(100); |
| fd = openPipeImpl(); |
| } |
| |
| return fd; |
| } |
| |
| private byte[] receiveMessage(final FileDescriptor fd) throws ErrnoException, |
| InterruptedIOException, EOFException, ProtocolException { |
| final byte[] lengthBits = new byte[4]; |
| readFully(fd, lengthBits, 0, lengthBits.length); |
| |
| final ByteBuffer bb = ByteBuffer.wrap(lengthBits); |
| bb.order(ByteOrder.LITTLE_ENDIAN); |
| final int msgLen = bb.getInt(); |
| |
| if (msgLen < 0 || msgLen > MAX_CLIPBOARD_BYTES) { |
| throw new ProtocolException("Clipboard message length: " + msgLen + " out of bounds."); |
| } |
| |
| final byte[] msg = new byte[msgLen]; |
| readFully(fd, msg, 0, msg.length); |
| |
| return msg; |
| } |
| |
| private static void sendMessage( |
| final FileDescriptor fd, |
| 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); |
| |
| writeFully(fd, lengthBits, 0, lengthBits.length); |
| writeFully(fd, msg, 0, msg.length); |
| } |
| |
| EmulatorClipboardMonitor(final Consumer<ClipData> setAndroidClipboard) { |
| this.mHostMonitorThread = new Thread(() -> { |
| FileDescriptor fd = null; |
| |
| while (!Thread.interrupted()) { |
| try { |
| if (fd == null) { |
| fd = openPipe(); |
| setPipeFD(fd); |
| } |
| |
| final byte[] receivedData = receiveMessage(fd); |
| |
| final String str = new String(receivedData); |
| final ClipData clip = new ClipData("host clipboard", |
| new String[]{"text/plain"}, |
| new ClipData.Item(str)); |
| final PersistableBundle bundle = new PersistableBundle(); |
| bundle.putBoolean("com.android.systemui.SUPPRESS_CLIPBOARD_OVERLAY", true); |
| clip.getDescription().setExtras(bundle); |
| |
| if (LOG_CLIBOARD_ACCESS) { |
| Slog.i(TAG, "Setting the guest clipboard to '" + str + "'"); |
| } |
| setAndroidClipboard.accept(clip); |
| } catch (ErrnoException | EOFException | InterruptedIOException |
| | InterruptedException | ProtocolException | OutOfMemoryError e) { |
| Slog.w(TAG, "Failure to read from host clipboard", e); |
| setPipeFD(null); |
| |
| try { |
| Os.close(fd); |
| } catch (ErrnoException e2) { |
| // ignore |
| } |
| |
| fd = null; |
| } |
| } |
| }); |
| |
| this.mHostMonitorThread.start(); |
| } |
| |
| @Override |
| public void accept(final @Nullable ClipData clip) { |
| final FileDescriptor fd = getPipeFD(); |
| if (fd != null) { |
| setHostClipboard(fd, getClipString(clip)); |
| } |
| } |
| |
| private String getClipString(final @Nullable ClipData clip) { |
| if (clip == null) { |
| return ""; |
| } |
| |
| if (clip.getItemCount() == 0) { |
| return ""; |
| } |
| |
| final CharSequence text = clip.getItemAt(0).getText(); |
| if (text == null) { |
| return ""; |
| } |
| |
| return text.toString(); |
| } |
| |
| private static void setHostClipboard(final FileDescriptor fd, final String value) { |
| Thread t = new Thread(() -> { |
| if (LOG_CLIBOARD_ACCESS) { |
| Slog.i(TAG, "Setting the host clipboard to '" + value + "'"); |
| } |
| |
| try { |
| sendMessage(fd, value.getBytes()); |
| } catch (ErrnoException | InterruptedIOException e) { |
| Slog.e(TAG, "Failed to set host clipboard " + e.getMessage()); |
| } catch (IllegalArgumentException e) { |
| } |
| }); |
| t.start(); |
| } |
| |
| private static void readFully(final FileDescriptor fd, |
| final byte[] buf, int offset, int size) |
| throws ErrnoException, InterruptedIOException, EOFException { |
| while (size > 0) { |
| final int r = Os.read(fd, buf, offset, size); |
| if (r > 0) { |
| offset += r; |
| size -= r; |
| } else { |
| throw new EOFException(); |
| } |
| } |
| } |
| |
| private static void writeFully(final FileDescriptor fd, |
| final byte[] buf, int offset, int size) |
| throws ErrnoException, InterruptedIOException { |
| while (size > 0) { |
| final int r = Os.write(fd, buf, offset, size); |
| if (r > 0) { |
| offset += r; |
| size -= r; |
| } else { |
| throw new ErrnoException("write", OsConstants.EIO); |
| } |
| } |
| } |
| } |