Refactor clipboard handling into a separate class
Bug: N/A
Test: N/A
Change-Id: I2e18d34ae6810e318e6607e4df343df5c65aff32
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java
new file mode 100644
index 0000000..828d923
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.util.Log;
+
+import java.nio.charset.StandardCharsets;
+
+/** Provide methods to synchronize clipboard across Android and VM. */
+class ClipboardHandler {
+ private static final String TAG = MainActivity.TAG;
+ private final ClipboardManager mClipboardManager;
+ private final VmAgent mVmAgent;
+
+ ClipboardHandler(Context context, VmAgent vmAgent) {
+ mClipboardManager = context.getSystemService(ClipboardManager.class);
+ mVmAgent = vmAgent;
+ }
+
+ private VmAgent.Connection getConnection() {
+ return mVmAgent.connect();
+ }
+
+ /** Read a text clip from Android's clipboard and send it to VM. */
+ void writeClipboardToVm() {
+ if (!mClipboardManager.hasPrimaryClip()) {
+ return;
+ }
+
+ ClipData clip = mClipboardManager.getPrimaryClip();
+ String text = clip.getItemAt(0).getText().toString();
+ // TODO: remove this trailing null character. The size is already encoded in the header.
+ text = text + '\0';
+ // TODO: use UTF-8 encoding
+ byte[] data = text.getBytes();
+
+ try {
+ getConnection().sendData(VmAgent.WRITE_CLIPBOARD_TYPE_TEXT_PLAIN, data);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Failed to write clipboard data to VM", e);
+ }
+ }
+
+ /** Read a text clip from VM and paste it to Android's clipboard. */
+ void readClipboardFromVm() {
+ VmAgent.Data data;
+ try {
+ data = getConnection().sendAndReceive(VmAgent.READ_CLIPBOARD_FROM_VM, null);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Failed to read clipboard data from VM", e);
+ return;
+ }
+
+ switch (data.type) {
+ case VmAgent.WRITE_CLIPBOARD_TYPE_EMPTY:
+ Log.d(TAG, "clipboard data from VM is empty");
+ break;
+ case VmAgent.WRITE_CLIPBOARD_TYPE_TEXT_PLAIN:
+ String text = new String(data.data, StandardCharsets.UTF_8);
+ ClipData clip = ClipData.newPlainText(null, text);
+ mClipboardManager.setPrimaryClip(clip);
+ break;
+ default:
+ Log.e(TAG, "Unknown clipboard response type: " + data.type);
+ }
+ }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
index 7c7d7ef..54543b0 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -20,11 +20,8 @@
import android.Manifest.permission;
import android.app.Activity;
-import android.content.ClipData;
-import android.content.ClipboardManager;
import android.content.Intent;
import android.os.Bundle;
-import android.os.ParcelFileDescriptor;
import android.system.virtualmachine.VirtualMachine;
import android.system.virtualmachine.VirtualMachineConfig;
import android.system.virtualmachine.VirtualMachineException;
@@ -35,14 +32,6 @@
import android.view.WindowInsets;
import android.view.WindowInsetsController;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -59,9 +48,10 @@
private ExecutorService mExecutorService;
private VirtualMachine mVirtualMachine;
- private ClipboardManager mClipboardManager;
private InputForwarder mInputForwarder;
private DisplayProvider mDisplayProvider;
+ private VmAgent mVmAgent;
+ private ClipboardHandler mClipboardHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -105,6 +95,9 @@
Path logPath = getFileStreamPath(mVirtualMachine.getName() + ".log").toPath();
Logger.setup(mVirtualMachine, logPath, mExecutorService);
+
+ mVmAgent = new VmAgent(mVirtualMachine);
+ mClipboardHandler = new ClipboardHandler(this, mVmAgent);
}
private void makeFullscreen() {
@@ -156,127 +149,25 @@
Log.d(TAG, "destroyed");
}
- private static final int DATA_SHARING_SERVICE_PORT = 3580;
- private static final byte READ_CLIPBOARD_FROM_VM = 0;
- private static final byte WRITE_CLIPBOARD_TYPE_EMPTY = 1;
- private static final byte WRITE_CLIPBOARD_TYPE_TEXT_PLAIN = 2;
- private static final byte OPEN_URL = 3;
-
- private ClipboardManager getClipboardManager() {
- if (mClipboardManager == null) {
- mClipboardManager = getSystemService(ClipboardManager.class);
- }
- return mClipboardManager;
- }
-
- // Construct header for the clipboard data.
- // Byte 0: Data type
- // Byte 1-3: Padding alignment & Reserved for other use cases in the future
- // Byte 4-7: Data size of the payload
- private byte[] constructClipboardHeader(byte type, int dataSize) {
- ByteBuffer header = ByteBuffer.allocate(8);
- header.clear();
- header.order(ByteOrder.LITTLE_ENDIAN);
- header.put(0, type);
- header.putInt(4, dataSize);
- return header.array();
- }
-
- private ParcelFileDescriptor connectDataSharingService() throws VirtualMachineException {
- // TODO(349702313): Consider when clipboard sharing server is started to run in VM.
- return mVirtualMachine.connectVsock(DATA_SHARING_SERVICE_PORT);
- }
-
- private void writeClipboardToVm() {
- Log.d(TAG, "running writeClipboardToVm");
- try (ParcelFileDescriptor pfd = connectDataSharingService()) {
- ClipboardManager clipboardManager = getClipboardManager();
- if (!clipboardManager.hasPrimaryClip()) {
- Log.d(TAG, "host device has no clipboard data");
- return;
- }
- ClipData clip = clipboardManager.getPrimaryClip();
- String text = clip.getItemAt(0).getText().toString();
-
- byte[] header =
- constructClipboardHeader(
- WRITE_CLIPBOARD_TYPE_TEXT_PLAIN, text.getBytes().length + 1);
- try (OutputStream stream = new FileOutputStream(pfd.getFileDescriptor())) {
- stream.write(header);
- stream.write(text.getBytes());
- stream.write('\0');
- Log.d(TAG, "successfully wrote clipboard data to the VM");
- } catch (IOException e) {
- Log.e(TAG, "failed to write clipboard data to the VM", e);
- }
- } catch (Exception e) {
- Log.e(TAG, "error on writeClipboardToVm", e);
- }
- }
-
- private byte[] readExactly(InputStream stream, int len) throws IOException {
- byte[] buf = stream.readNBytes(len);
- if (buf.length != len) {
- throw new IOException("Cannot read enough bytes");
- }
- return buf;
- }
-
- private void readClipboardFromVm() {
- Log.d(TAG, "running readClipboardFromVm");
- try (ParcelFileDescriptor pfd = connectDataSharingService()) {
- byte[] request = constructClipboardHeader(READ_CLIPBOARD_FROM_VM, 0);
- try (OutputStream output = new FileOutputStream(pfd.getFileDescriptor())) {
- output.write(request);
- Log.d(TAG, "successfully send request to the VM for reading clipboard");
- } catch (IOException e) {
- Log.e(TAG, "failed to send request to the VM for reading clipboard");
- throw e;
- }
-
- try (InputStream input = new FileInputStream(pfd.getFileDescriptor())) {
- ByteBuffer header = ByteBuffer.wrap(readExactly(input, 8));
- header.order(ByteOrder.LITTLE_ENDIAN);
- switch (header.get(0)) {
- case WRITE_CLIPBOARD_TYPE_EMPTY:
- Log.d(TAG, "clipboard data in VM is empty");
- break;
- case WRITE_CLIPBOARD_TYPE_TEXT_PLAIN:
- int dataSize = header.getInt(4);
- String text_data =
- new String(readExactly(input, dataSize), StandardCharsets.UTF_8);
- getClipboardManager()
- .setPrimaryClip(ClipData.newPlainText(null, text_data));
- Log.d(TAG, "successfully received clipboard data from VM");
- break;
- default:
- Log.e(TAG, "unknown clipboard response type");
- break;
- }
- } catch (IOException e) {
- Log.e(TAG, "failed to receive clipboard content from VM");
- throw e;
- }
- } catch (Exception e) {
- Log.e(TAG, "error on readClipboardFromVm", e);
- }
- }
-
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
+
+ // TODO: explain why we have to do this on every focus change
if (hasFocus) {
- SurfaceView surfaceView = findViewById(R.id.surface_view);
- Log.d(TAG, "requestPointerCapture()");
- surfaceView.requestPointerCapture();
+ SurfaceView mainView = findViewById(R.id.surface_view);
+ mainView.requestPointerCapture();
}
- if (mVirtualMachine != null) {
- if (hasFocus) {
- mExecutorService.execute(() -> writeClipboardToVm());
- } else {
- mExecutorService.execute(() -> readClipboardFromVm());
- }
- }
+
+ // TODO: remove executor here. Let clipboard handler handle this.
+ mExecutorService.execute(
+ () -> {
+ if (hasFocus) {
+ mClipboardHandler.writeClipboardToVm();
+ } else {
+ mClipboardHandler.readClipboardFromVm();
+ }
+ });
}
@Override
@@ -291,16 +182,7 @@
if (text != null) {
mExecutorService.execute(
() -> {
- byte[] data = text.getBytes();
- try (ParcelFileDescriptor pfd = connectDataSharingService();
- OutputStream stream =
- new FileOutputStream(pfd.getFileDescriptor())) {
- stream.write(constructClipboardHeader(OPEN_URL, data.length));
- stream.write(data);
- Log.d(TAG, "Successfully sent URL to the VM");
- } catch (IOException | VirtualMachineException e) {
- Log.e(TAG, "Failed to send URL to the VM", e);
- }
+ mVmAgent.connect().sendData(VmAgent.OPEN_URL, text.getBytes());
});
}
}
@@ -312,5 +194,4 @@
new String[] {permission.RECORD_AUDIO}, RECORD_AUDIO_PERMISSION_REQUEST_CODE);
}
}
-
}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java
new file mode 100644
index 0000000..78da6c0
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.os.ParcelFileDescriptor;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineException;
+
+import libcore.io.Streams;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Agent running in the VM. This class provides connection to the agent and ways to communicate with
+ * it.
+ */
+class VmAgent {
+ private static final String TAG = MainActivity.TAG;
+ private static final int DATA_SHARING_SERVICE_PORT = 3580;
+ private static final int HEADER_SIZE = 8; // size of the header
+ private static final int SIZE_OFFSET = 4; // offset of the size field in the header
+
+ static final byte READ_CLIPBOARD_FROM_VM = 0;
+ static final byte WRITE_CLIPBOARD_TYPE_EMPTY = 1;
+ static final byte WRITE_CLIPBOARD_TYPE_TEXT_PLAIN = 2;
+ static final byte OPEN_URL = 3;
+
+ private final VirtualMachine mVirtualMachine;
+
+ VmAgent(VirtualMachine vm) {
+ mVirtualMachine = vm;
+ }
+
+ /** Connect to the agent and returns the communication channel established. This can block. */
+ Connection connect() {
+ try {
+ // TODO: wait until the VM is up and the agent is running
+ return new Connection(mVirtualMachine.connectVsock(DATA_SHARING_SERVICE_PORT));
+ } catch (VirtualMachineException e) {
+ throw new RuntimeException("Failed to connect to the VM agent", e);
+ }
+ }
+
+ static class Data {
+ final int type;
+ final byte[] data;
+
+ Data(int type, byte[] data) {
+ this.type = type;
+ this.data = data;
+ }
+ }
+
+ /** Represents a connection to the agent */
+ class Connection {
+ private final ParcelFileDescriptor mConn;
+
+ private Connection(ParcelFileDescriptor conn) {
+ mConn = conn;
+ }
+
+ /** Send data of a given type. This can block. */
+ void sendData(byte type, byte[] data) {
+ // Byte 0: Data type
+ // Byte 1-3: Padding alignment & Reserved for other use cases in the future
+ // Byte 4-7: Data size of the payload
+ ByteBuffer header = ByteBuffer.allocate(HEADER_SIZE);
+ header.clear();
+ header.order(ByteOrder.LITTLE_ENDIAN);
+ header.put(0, type);
+ int dataSize = data == null ? 0 : data.length;
+ header.putInt(SIZE_OFFSET, dataSize);
+
+ try (OutputStream out = new FileOutputStream(mConn.getFileDescriptor())) {
+ out.write(header.array());
+ if (data != null) {
+ out.write(data);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to send message of type: " + type, e);
+ }
+ }
+
+ /** Read data from agent. This can block. */
+ Data readData() {
+ ByteBuffer header = ByteBuffer.allocate(HEADER_SIZE);
+ header.clear();
+ header.order(ByteOrder.LITTLE_ENDIAN);
+ byte[] data;
+
+ try (InputStream in = new FileInputStream(mConn.getFileDescriptor())) {
+ Streams.readFully(in, header.array());
+ byte type = header.get(0);
+ int dataSize = header.getInt(SIZE_OFFSET);
+ data = new byte[dataSize];
+ Streams.readFully(in, data);
+ return new Data(type, data);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to read data", e);
+ }
+ }
+
+ /** Convenient method for sending data and then reading response for it. This can block. */
+ Data sendAndReceive(byte type, byte[] data) {
+ sendData(type, data);
+ return readData();
+ }
+ }
+}