Add FerrochromeApp as a share target
Allow sharing text data to FerrochromeApp, if text looks like an URL
(supports http / https / mailto link for now), open the link in VM.
Add a new OpenUrlActivity in FerrochromeApp to handle this feature.
OpenUrlActivity is launched with the 'singleTask' launch mode and this
ensures it is instantiated in the same taskAffinity task as the
FerrochromeActivity. It then bounces the share Intent to the already
launched VM activity.
Change launch mode of VmLauncherApp to 'singleTop' so that we can send
Intent to the already launched VM activity. If we don't use 'singleTop'
launch mode then we would instead create multiple instance of the VM
activity. When the VM activity receives a intent via onNewIntent(), it
sends the Intent payload to the VM data sharing service.
KI: VM need to be logged in first for link sharing to work.
Bug: 348303697
Test: 1. Launch FerrochromeApp and login
2. Switch to another app to select a link
3. Share the link to FerrochromeApp
Change-Id: I39742d4e897caaa37595f50697540196d0393946
diff --git a/ferrochrome_app/AndroidManifest.xml b/ferrochrome_app/AndroidManifest.xml
index 826fcc4..7afffe5 100644
--- a/ferrochrome_app/AndroidManifest.xml
+++ b/ferrochrome_app/AndroidManifest.xml
@@ -23,6 +23,16 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
+ <activity android:name=".OpenUrlActivity"
+ android:theme="@android:style/Theme.NoDisplay"
+ android:launchMode="singleTask"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.SEND" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="text/*" />
+ </intent-filter>
+ </activity>
</application>
</manifest>
diff --git a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
index 5006413..2df5cab 100644
--- a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
+++ b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
@@ -55,11 +55,18 @@
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ if (!isTaskRoot()) {
+ // In case we launched this activity multiple times, only start one instance of this
+ // activity by only starting this as the root activity in task.
+ finish();
+ Log.w(TAG, "Not starting because not task root");
+ return;
+ }
setContentView(R.layout.activity_ferrochrome);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// Find VM Launcher
- Intent intent = new Intent(ACTION_VM_LAUNCHER);
+ Intent intent = new Intent(ACTION_VM_LAUNCHER).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PackageManager pm = getPackageManager();
List<ResolveInfo> resolveInfos =
pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
diff --git a/ferrochrome_app/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java b/ferrochrome_app/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
new file mode 100644
index 0000000..c32d017
--- /dev/null
+++ b/ferrochrome_app/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
@@ -0,0 +1,69 @@
+/*
+ * 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.ferrochrome;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+public class OpenUrlActivity extends Activity {
+ private static final String TAG = OpenUrlActivity.class.getSimpleName();
+
+ private static final String ACTION_VM_OPEN_URL = "android.virtualization.VM_OPEN_URL";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ boolean isRoot = isTaskRoot();
+ finish();
+ if (!Intent.ACTION_SEND.equals(getIntent().getAction())) {
+ return;
+ }
+ String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
+ if (text == null) {
+ return;
+ }
+ Uri uri = Uri.parse(text);
+ if (uri == null) {
+ return;
+ }
+ String scheme = uri.getScheme();
+ if (!("http".equals(scheme) || "https".equals(scheme) || "mailto".equals(scheme))) {
+ Log.e(TAG, "Unsupported URL scheme: " + scheme);
+ return;
+ }
+ Log.i(TAG, "Sending " + scheme + " URL to VM");
+ if (isRoot) {
+ Log.w(
+ TAG,
+ "Cannot open URL without starting "
+ + FerrochromeActivity.class.getSimpleName()
+ + " first, starting it now");
+ startActivity(
+ new Intent(this, FerrochromeActivity.class).setAction(Intent.ACTION_MAIN));
+ return;
+ }
+ startActivity(
+ new Intent(ACTION_VM_OPEN_URL)
+ .setFlags(
+ Intent.FLAG_ACTIVITY_SINGLE_TOP
+ | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP)
+ .putExtra(Intent.EXTRA_TEXT, text));
+ }
+}
diff --git a/vmlauncher_app/AndroidManifest.xml b/vmlauncher_app/AndroidManifest.xml
index e7a23dc..c6ab1f2 100644
--- a/vmlauncher_app/AndroidManifest.xml
+++ b/vmlauncher_app/AndroidManifest.xml
@@ -17,8 +17,9 @@
android:exported="true">
<intent-filter>
<action android:name="android.virtualization.VM_LAUNCHER" />
+ <action android:name="android.virtualization.VM_OPEN_URL" />
<category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
+ </intent-filter>
</activity>
</application>
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 105ae03..0b93968 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -23,6 +23,7 @@
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
+import android.content.Intent;
import android.crosvm.ICrosvmAndroidDisplayService;
import android.graphics.PixelFormat;
import android.graphics.Rect;
@@ -80,12 +81,17 @@
public class MainActivity extends Activity implements InputManager.InputDeviceListener {
private static final String TAG = "VmLauncherApp";
private static final String VM_NAME = "my_custom_vm";
+
private static final boolean DEBUG = true;
+ private static final int RECORD_AUDIO_PERMISSION_REQUEST_CODE = 101;
+
+ private static final String ACTION_VM_LAUNCHER = "android.virtualization.VM_LAUNCHER";
+ private static final String ACTION_VM_OPEN_URL = "android.virtualization.VM_OPEN_URL";
+
private ExecutorService mExecutorService;
private VirtualMachine mVirtualMachine;
private CursorHandler mCursorHandler;
private ClipboardManager mClipboardManager;
- private static final int RECORD_AUDIO_PERMISSION_REQUEST_CODE = 101;
private VirtualMachineConfig createVirtualMachineConfig(String jsonPath) {
VirtualMachineConfig.Builder configBuilder =
@@ -321,6 +327,12 @@
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ String action = getIntent().getAction();
+ if (!ACTION_VM_LAUNCHER.equals(action)) {
+ finish();
+ Log.e(TAG, "onCreate unsupported intent action: " + action);
+ return;
+ }
checkAndRequestRecordAudioPermission();
mExecutorService = Executors.newCachedThreadPool();
try {
@@ -571,6 +583,7 @@
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) {
@@ -688,6 +701,32 @@
}
}
+ @Override
+ protected void onNewIntent(Intent intent) {
+ String action = intent.getAction();
+ if (!ACTION_VM_OPEN_URL.equals(action)) {
+ Log.e(TAG, "onNewIntent unsupported intent action: " + action);
+ return;
+ }
+ Log.d(TAG, "onNewIntent intent action: " + action);
+ String text = intent.getStringExtra(Intent.EXTRA_TEXT);
+ 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);
+ }
+ });
+ }
+ }
+
@FunctionalInterface
public interface RemoteExceptionCheckedFunction<T> {
void apply(T t) throws RemoteException;