From bc3f6fa5c00e861abf4685068f8c53ab88cbf151 Mon Sep 17 00:00:00 2001 From: Shaun Corkran Date: Sun, 27 Nov 2022 01:22:55 +0000 Subject: [PATCH] Addition of native handoff of an intercepted intent matched according to a provided IntentFilter. API user can register and unregister an intent interceptor through the VirtualDevice by passing in an IntentFilter to match an intent. When the passed in intent matches, the intent will be intercepted and sent to the VirtualDevice API user while the intended local activity will be aborted. Bug: 237537362 Test: manual Test: atest WmTests:DisplayWindowPolicyControllerTests Test: atest VirtualDeviceManagerServiceTest Test: atest CtsVirtualDevicesTestCases Change-Id: Id841190045b68b9723e58ca02c365cf1c24df8c6 --- core/api/system-current.txt | 6 ++ .../companion/virtual/IVirtualDevice.aidl | 13 +++ .../IVirtualDeviceIntentInterceptor.aidl | 39 ++++++++ .../virtual/VirtualDeviceManager.java | 99 +++++++++++++++++++ .../window/DisplayWindowPolicyController.java | 3 +- .../GenericWindowPolicyController.java | 22 ++++- .../companion/virtual/VirtualDeviceImpl.java | 58 +++++++++++ .../android/server/wm/ActivityStarter.java | 4 +- .../DisplayWindowPolicyControllerHelper.java | 9 +- .../VirtualDeviceManagerServiceTest.java | 99 +++++++++++++++++++ .../audio/VirtualAudioControllerTest.java | 1 + .../DisplayWindowPolicyControllerTests.java | 3 +- 12 files changed, 346 insertions(+), 10 deletions(-) create mode 100644 core/java/android/companion/virtual/IVirtualDeviceIntentInterceptor.aidl diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 566ac4535b0d1..2cbf7ef0d6607 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2988,6 +2988,10 @@ package android.companion.virtual { method public void onTopActivityChanged(int, @NonNull android.content.ComponentName); } + public static interface VirtualDeviceManager.IntentInterceptorCallback { + method public void onIntentIntercepted(@NonNull android.content.Intent); + } + public static class VirtualDeviceManager.VirtualDevice implements java.lang.AutoCloseable { method public void addActivityListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener); method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close(); @@ -3006,8 +3010,10 @@ package android.companion.virtual { method public int getDeviceId(); method @Nullable public android.companion.virtual.sensor.VirtualSensor getVirtualSensor(int, @NonNull String); method public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer); + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void registerIntentInterceptor(@NonNull java.util.concurrent.Executor, @NonNull android.content.IntentFilter, @NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback); method public void removeActivityListener(@NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener); method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean); + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void unregisterIntentInterceptor(@NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback); } public final class VirtualDeviceParams implements android.os.Parcelable { diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl index f17d18652e98d..22ea9f2094cb8 100644 --- a/core/java/android/companion/virtual/IVirtualDevice.aidl +++ b/core/java/android/companion/virtual/IVirtualDevice.aidl @@ -17,11 +17,13 @@ package android.companion.virtual; import android.app.PendingIntent; +import android.companion.virtual.IVirtualDeviceIntentInterceptor; import android.companion.virtual.audio.IAudioConfigChangedCallback; import android.companion.virtual.audio.IAudioRoutingCallback; import android.companion.virtual.sensor.IVirtualSensorStateChangeCallback; import android.companion.virtual.sensor.VirtualSensorConfig; import android.companion.virtual.sensor.VirtualSensorEvent; +import android.content.IntentFilter; import android.graphics.Point; import android.graphics.PointF; import android.hardware.input.VirtualDpadConfig; @@ -125,4 +127,15 @@ interface IVirtualDevice { /** Sets whether to show or hide the cursor while this virtual device is active. */ void setShowPointerIcon(boolean showPointerIcon); + + /** + * Registers an intent interceptor that will intercept an intent attempting to launch + * when matching the provided IntentFilter and calls the callback with the intercepted + * intent. + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + void registerIntentInterceptor( + in IVirtualDeviceIntentInterceptor intentInterceptor, in IntentFilter filter); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + void unregisterIntentInterceptor(in IVirtualDeviceIntentInterceptor intentInterceptor); } diff --git a/core/java/android/companion/virtual/IVirtualDeviceIntentInterceptor.aidl b/core/java/android/companion/virtual/IVirtualDeviceIntentInterceptor.aidl new file mode 100644 index 0000000000000..cd43bf746ecf5 --- /dev/null +++ b/core/java/android/companion/virtual/IVirtualDeviceIntentInterceptor.aidl @@ -0,0 +1,39 @@ +/* + * Copyright 2022 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 android.companion.virtual; + +import android.content.Intent; + +/** + * Interceptor interface to be called when an intent matches the IntentFilter passed into {@link + * VirtualDevice#registerIntentInterceptor}. When the interceptor is called after matching the + * IntentFilter, the intended activity launch will be aborted and alternatively replaced by + * the interceptor's receiver. + * + * @hide + */ +oneway interface IVirtualDeviceIntentInterceptor { + + /** + * Called when an intent that matches the IntentFilter registered in {@link + * VirtualDevice#registerIntentInterceptor} is intercepted for the virtual device to + * handle. + * + * @param intent The intent that has been intercepted by the interceptor. + */ + void onIntentIntercepted(in Intent intent); +} diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index dba7c8e630c80..cc452e27a650c 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -35,6 +35,8 @@ import android.companion.virtual.sensor.VirtualSensor; import android.companion.virtual.sensor.VirtualSensorConfig; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.graphics.Point; import android.hardware.camera2.CameraCharacteristics; import android.hardware.display.DisplayManager; @@ -269,6 +271,9 @@ public final class VirtualDeviceManager { private final IVirtualDevice mVirtualDevice; private final ArrayMap mActivityListeners = new ArrayMap<>(); + private final ArrayMap mIntentInterceptorListeners = + new ArrayMap<>(); private final IVirtualDeviceActivityListener mActivityListenerBinder = new IVirtualDeviceActivityListener.Stub() { @@ -857,6 +862,53 @@ public final class VirtualDeviceManager { public void removeActivityListener(@NonNull ActivityListener listener) { mActivityListeners.remove(listener); } + + /** + * Registers an intent interceptor that will intercept an intent attempting to launch + * when matching the provided IntentFilter and calls the callback with the intercepted + * intent. + * + * @param executor The executor where the interceptor is executed on. + * @param interceptorFilter The filter to match intents intended for interception. + * @param interceptorCallback The callback called when an intent matching interceptorFilter + * is intercepted. + * @see #unregisterIntentInterceptor(IntentInterceptorCallback) + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void registerIntentInterceptor( + @CallbackExecutor @NonNull Executor executor, + @NonNull IntentFilter interceptorFilter, + @NonNull IntentInterceptorCallback interceptorCallback) { + Objects.requireNonNull(executor); + Objects.requireNonNull(interceptorFilter); + Objects.requireNonNull(interceptorCallback); + final VirtualIntentInterceptorDelegate delegate = + new VirtualIntentInterceptorDelegate(executor, interceptorCallback); + try { + mVirtualDevice.registerIntentInterceptor(delegate, interceptorFilter); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mIntentInterceptorListeners.put(interceptorCallback, delegate); + } + + /** + * Unregisters the intent interceptorCallback previously registered with + * {@link #registerIntentInterceptor}. + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void unregisterIntentInterceptor( + @NonNull IntentInterceptorCallback interceptorCallback) { + Objects.requireNonNull(interceptorCallback); + final VirtualIntentInterceptorDelegate delegate = + mIntentInterceptorListeners.get(interceptorCallback); + try { + mVirtualDevice.unregisterIntentInterceptor(delegate); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mIntentInterceptorListeners.remove(interceptorCallback); + } } /** @@ -908,4 +960,51 @@ public final class VirtualDeviceManager { mExecutor.execute(() -> mActivityListener.onDisplayEmpty(displayId)); } } + + /** + * Interceptor interface to be called when an intent matches the IntentFilter passed into {@link + * VirtualDevice#registerIntentInterceptor}. When the interceptor is called after matching the + * IntentFilter, the intended activity launch will be aborted and alternatively replaced by + * the interceptor's receiver. + * + * @hide + */ + @SystemApi + public interface IntentInterceptorCallback { + + /** + * Called when an intent that matches the IntentFilter registered in {@link + * VirtualDevice#registerIntentInterceptor} is intercepted for the virtual device to + * handle. + * + * @param intent The intent that has been intercepted by the interceptor. + */ + void onIntentIntercepted(@NonNull Intent intent); + } + + /** + * A wrapper for {@link IntentInterceptorCallback} that executes callbacks on the + * the given executor. + */ + private static class VirtualIntentInterceptorDelegate + extends IVirtualDeviceIntentInterceptor.Stub { + @NonNull private final IntentInterceptorCallback mIntentInterceptorCallback; + @NonNull private final Executor mExecutor; + + private VirtualIntentInterceptorDelegate(Executor executor, + IntentInterceptorCallback interceptorCallback) { + mExecutor = executor; + mIntentInterceptorCallback = interceptorCallback; + } + + @Override + public void onIntentIntercepted(Intent intent) { + final long token = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mIntentInterceptorCallback.onIntentIntercepted(intent)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } } diff --git a/core/java/android/window/DisplayWindowPolicyController.java b/core/java/android/window/DisplayWindowPolicyController.java index e027934ccb515..535dc4e78c89d 100644 --- a/core/java/android/window/DisplayWindowPolicyController.java +++ b/core/java/android/window/DisplayWindowPolicyController.java @@ -21,6 +21,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import android.annotation.NonNull; import android.app.WindowConfiguration; import android.content.ComponentName; +import android.content.Intent; import android.content.pm.ActivityInfo; import android.util.ArraySet; @@ -114,7 +115,7 @@ public abstract class DisplayWindowPolicyController { /** * Returns {@code true} if the given new task can be launched on this virtual display. */ - public abstract boolean canActivityBeLaunched(@NonNull ActivityInfo activityInfo, + public abstract boolean canActivityBeLaunched(@NonNull ActivityInfo activityInfo, Intent intent, @WindowConfiguration.WindowingMode int windowingMode, int launchingFromDisplayId, boolean isNewTask); diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java index 5e68d5231c6b7..6b2e893a55c9d 100644 --- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java +++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java @@ -32,6 +32,7 @@ import android.companion.virtual.VirtualDeviceParams.RecentsPolicy; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.ComponentName; +import android.content.Intent; import android.content.pm.ActivityInfo; import android.os.Build; import android.os.Handler; @@ -93,6 +94,12 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController void onEnteringPipBlocked(int uid); } + /** Interface to listen for interception of intents. */ + public interface IntentListenerCallback { + /** Returns true when an intent should be intercepted */ + boolean shouldInterceptIntent(Intent intent); + } + /** * If required, allow the secure activity to display on remote device since * {@link android.os.Build.VERSION_CODES#TIRAMISU}. @@ -121,6 +128,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController final ArraySet mRunningUids = new ArraySet<>(); @Nullable private final ActivityListener mActivityListener; @Nullable private final PipBlockedCallback mPipBlockedCallback; + @Nullable private final IntentListenerCallback mIntentListenerCallback; private final Handler mHandler = new Handler(Looper.getMainLooper()); @NonNull @GuardedBy("mGenericWindowPolicyControllerLock") @@ -155,6 +163,8 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController * launching. * @param secureWindowCallback Callback that is called when a secure window shows on the * virtual display. + * @param intentListenerCallback Callback that is called to intercept intents when matching + * passed in filters. * @param defaultRecentsPolicy a policy to indicate how to handle activities in recents. */ public GenericWindowPolicyController(int windowFlags, int systemWindowFlags, @@ -168,6 +178,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController @NonNull PipBlockedCallback pipBlockedCallback, @NonNull ActivityBlockedCallback activityBlockedCallback, @NonNull SecureWindowCallback secureWindowCallback, + @NonNull IntentListenerCallback intentListenerCallback, @NonNull List displayCategories, @RecentsPolicy int defaultRecentsPolicy) { super(); @@ -182,6 +193,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController mActivityListener = activityListener; mPipBlockedCallback = pipBlockedCallback; mSecureWindowCallback = secureWindowCallback; + mIntentListenerCallback = intentListenerCallback; mDisplayCategories = displayCategories; mDefaultRecentsPolicy = defaultRecentsPolicy; } @@ -227,8 +239,8 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController @Override public boolean canActivityBeLaunched(ActivityInfo activityInfo, - @WindowConfiguration.WindowingMode int windowingMode, int launchingFromDisplayId, - boolean isNewTask) { + Intent intent, @WindowConfiguration.WindowingMode int windowingMode, + int launchingFromDisplayId, boolean isNewTask) { if (!isWindowingModeSupported(windowingMode)) { return false; } @@ -261,6 +273,12 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController return false; } + if (mIntentListenerCallback != null && intent != null + && mIntentListenerCallback.shouldInterceptIntent(intent)) { + Slog.d(TAG, "Virtual device has intercepted intent"); + return false; + } + return true; } diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 12ad9f1cf5805..29cde8ebd5138 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -33,6 +33,7 @@ import android.app.admin.DevicePolicyManager; import android.companion.AssociationInfo; import android.companion.virtual.IVirtualDevice; import android.companion.virtual.IVirtualDeviceActivityListener; +import android.companion.virtual.IVirtualDeviceIntentInterceptor; import android.companion.virtual.VirtualDeviceManager; import android.companion.virtual.VirtualDeviceManager.ActivityListener; import android.companion.virtual.VirtualDeviceParams; @@ -43,6 +44,7 @@ import android.companion.virtual.sensor.VirtualSensorEvent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.graphics.PointF; import android.hardware.display.DisplayManager; @@ -114,6 +116,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private final VirtualDeviceParams mParams; private final Map mPerDisplayWakelocks = new ArrayMap<>(); private final IVirtualDeviceActivityListener mActivityListener; + @GuardedBy("mVirtualDeviceLock") + private final Map mIntentInterceptors = new ArrayMap<>(); @NonNull private Consumer> mRunningAppsChangedCallback; // The default setting for showing the pointer on new displays. @@ -680,6 +684,31 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } } + @Override // Binder call + public void registerIntentInterceptor(IVirtualDeviceIntentInterceptor intentInterceptor, + IntentFilter filter) { + Objects.requireNonNull(intentInterceptor); + Objects.requireNonNull(filter); + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CREATE_VIRTUAL_DEVICE, + "Permission required to register intent interceptor"); + synchronized (mVirtualDeviceLock) { + mIntentInterceptors.put(intentInterceptor.asBinder(), filter); + } + } + + @Override // Binder call + public void unregisterIntentInterceptor( + @NonNull IVirtualDeviceIntentInterceptor intentInterceptor) { + Objects.requireNonNull(intentInterceptor); + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CREATE_VIRTUAL_DEVICE, + "Permission required to unregister intent interceptor"); + synchronized (mVirtualDeviceLock) { + mIntentInterceptors.remove(intentInterceptor.asBinder()); + } + } + @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { fout.println(" VirtualDevice: "); @@ -713,6 +742,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub this::onEnteringPipBlocked, this::onActivityBlocked, this::onSecureWindowShown, + this::shouldInterceptIntent, displayCategories, mParams.getDefaultRecentsPolicy()); gwpc.registerRunningAppsChangedListener(/* listener= */ this); @@ -872,6 +902,34 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub Toast.LENGTH_LONG, mContext.getMainLooper()); } + /** + * Intercepts intent when matching any of the IntentFilter of any interceptor. Returns true if + * the intent matches any filter notifying the DisplayPolicyController to abort the + * activity launch to be replaced by the interception. + */ + private boolean shouldInterceptIntent(Intent intent) { + synchronized (mVirtualDeviceLock) { + boolean hasInterceptedIntent = false; + for (Map.Entry interceptor : mIntentInterceptors.entrySet()) { + if (interceptor.getValue().match( + intent.getAction(), intent.getType(), intent.getScheme(), intent.getData(), + intent.getCategories(), TAG) >= 0) { + try { + // For privacy reasons, only returning the intents action and data. Any + // other required field will require a review. + IVirtualDeviceIntentInterceptor.Stub.asInterface(interceptor.getKey()) + .onIntentIntercepted(new Intent(intent.getAction(), intent.getData())); + hasInterceptedIntent = true; + } catch (RemoteException e) { + Slog.w(TAG, "Unable to call mVirtualDeviceIntentInterceptor", e); + } + } + } + + return hasInterceptedIntent; + } + } + interface OnDeviceCloseListener { void onClose(int deviceId); } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 2762c459fba45..6c49f47e68803 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1829,8 +1829,8 @@ class ActivityStarter { final int launchingFromDisplayId = mSourceRecord != null ? mSourceRecord.getDisplayId() : DEFAULT_DISPLAY; if (!displayContent.mDwpcHelper - .canActivityBeLaunched(r.info, targetWindowingMode, launchingFromDisplayId, - newTask)) { + .canActivityBeLaunched(r.info, r.intent, targetWindowingMode, + launchingFromDisplayId, newTask)) { Slog.w(TAG, "Abort to launch " + r.info.getComponentName() + " on display area " + mPreferredTaskDisplayArea); return START_ABORTED; diff --git a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java index 72ed1083043b2..154fb0cecac32 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java +++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java @@ -19,6 +19,7 @@ package com.android.server.wm; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.WindowConfiguration; +import android.content.Intent; import android.content.pm.ActivityInfo; import android.os.UserHandle; import android.util.ArraySet; @@ -91,8 +92,8 @@ class DisplayWindowPolicyControllerHelper { * @see DisplayWindowPolicyController#canActivityBeLaunched(ActivityInfo, int, int, boolean) */ public boolean canActivityBeLaunched(ActivityInfo activityInfo, - @WindowConfiguration.WindowingMode int windowingMode, int launchingFromDisplayId, - boolean isNewTask) { + Intent intent, @WindowConfiguration.WindowingMode int windowingMode, + int launchingFromDisplayId, boolean isNewTask) { if (mDisplayWindowPolicyController == null) { if (activityInfo.requiredDisplayCategory != null) { Slog.e(TAG, @@ -104,8 +105,8 @@ class DisplayWindowPolicyControllerHelper { } return true; } - return mDisplayWindowPolicyController.canActivityBeLaunched(activityInfo, windowingMode, - launchingFromDisplayId, isNewTask); + return mDisplayWindowPolicyController.canActivityBeLaunched(activityInfo, intent, + windowingMode, launchingFromDisplayId, isNewTask); } /** diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 31e53d56f5202..ad7550e47712e 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -21,6 +21,7 @@ import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_INVALID; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS; +import static android.content.Intent.ACTION_VIEW; import static android.content.pm.ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES; import static com.google.common.truth.Truth.assertThat; @@ -37,6 +38,7 @@ import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -49,6 +51,7 @@ import android.app.WindowConfiguration; import android.app.admin.DevicePolicyManager; import android.companion.AssociationInfo; import android.companion.virtual.IVirtualDeviceActivityListener; +import android.companion.virtual.IVirtualDeviceIntentInterceptor; import android.companion.virtual.VirtualDeviceParams; import android.companion.virtual.audio.IAudioConfigChangedCallback; import android.companion.virtual.audio.IAudioRoutingCallback; @@ -57,6 +60,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.hardware.Sensor; @@ -73,6 +77,7 @@ import android.hardware.input.VirtualNavigationTouchpadConfig; import android.hardware.input.VirtualTouchEvent; import android.hardware.input.VirtualTouchscreenConfig; import android.net.MacAddress; +import android.net.Uri; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -184,6 +189,7 @@ public class VirtualDeviceManagerServiceTest { .setInputDeviceName(DEVICE_NAME) .setAssociatedDisplayId(DISPLAY_ID) .build(); + private static final String TEST_SITE = "http://test"; private Context mContext; private InputManagerMockHelper mInputManagerMockHelper; @@ -1339,6 +1345,99 @@ public class VirtualDeviceManagerServiceTest { assertThat(gwpc.getRunningAppsChangedListenersSizeForTesting()).isEqualTo(0); } + @Test + public void canActivityBeLaunched_activityCanLaunch() { + Intent intent = new Intent(ACTION_VIEW, Uri.parse(TEST_SITE)); + mDeviceImpl.onVirtualDisplayCreatedLocked( + mDeviceImpl.createWindowPolicyController(new ArrayList<>()), DISPLAY_ID); + GenericWindowPolicyController gwpc = mDeviceImpl.getWindowPolicyControllersForTesting().get( + DISPLAY_ID); + ArrayList activityInfos = getActivityInfoList( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoveDevices */ true, + /* targetDisplayCategory */ null); + assertThat(gwpc.canActivityBeLaunched(activityInfos.get(0), intent, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID, /*isNewTask=*/false)) + .isTrue(); + } + + @Test + public void canActivityBeLaunched_intentInterceptedWhenRegistered_activityNoLaunch() + throws RemoteException { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(TEST_SITE)); + + IVirtualDeviceIntentInterceptor.Stub interceptor = + mock(IVirtualDeviceIntentInterceptor.Stub.class); + doNothing().when(interceptor).onIntentIntercepted(any()); + doReturn(interceptor).when(interceptor).asBinder(); + doReturn(interceptor).when(interceptor).queryLocalInterface(anyString()); + + mDeviceImpl.onVirtualDisplayCreatedLocked( + mDeviceImpl.createWindowPolicyController(new ArrayList<>()), DISPLAY_ID); + GenericWindowPolicyController gwpc = mDeviceImpl.getWindowPolicyControllersForTesting().get( + DISPLAY_ID); + ArrayList activityInfos = getActivityInfoList( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoveDevices */ true, + /* targetDisplayCategory */ null); + + IntentFilter intentFilter = new IntentFilter(Intent.ACTION_VIEW); + intentFilter.addDataScheme(IntentFilter.SCHEME_HTTP); + intentFilter.addDataScheme(IntentFilter.SCHEME_HTTPS); + + // register interceptor and intercept intent + mDeviceImpl.registerIntentInterceptor(interceptor, intentFilter); + assertThat(gwpc.canActivityBeLaunched(activityInfos.get(0), intent, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID, /*isNewTask=*/false)) + .isFalse(); + ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(interceptor).onIntentIntercepted(intentCaptor.capture()); + Intent cIntent = intentCaptor.getValue(); + assertThat(cIntent).isNotNull(); + assertThat(cIntent.getAction()).isEqualTo(Intent.ACTION_VIEW); + assertThat(cIntent.getData().toString()).isEqualTo(TEST_SITE); + + // unregister interceptor and launch activity + mDeviceImpl.unregisterIntentInterceptor(interceptor); + assertThat(gwpc.canActivityBeLaunched(activityInfos.get(0), intent, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID, /*isNewTask=*/false)) + .isTrue(); + } + + @Test + public void canActivityBeLaunched_noMatchIntentFilter_activityLaunches() + throws RemoteException { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("testing")); + + IVirtualDeviceIntentInterceptor.Stub interceptor = + mock(IVirtualDeviceIntentInterceptor.Stub.class); + doNothing().when(interceptor).onIntentIntercepted(any()); + doReturn(interceptor).when(interceptor).asBinder(); + doReturn(interceptor).when(interceptor).queryLocalInterface(anyString()); + + mDeviceImpl.onVirtualDisplayCreatedLocked( + mDeviceImpl.createWindowPolicyController(new ArrayList<>()), DISPLAY_ID); + GenericWindowPolicyController gwpc = mDeviceImpl.getWindowPolicyControllersForTesting().get( + DISPLAY_ID); + ArrayList activityInfos = getActivityInfoList( + NONBLOCKED_APP_PACKAGE_NAME, + NONBLOCKED_APP_PACKAGE_NAME, + /* displayOnRemoveDevices */ true, + /* targetDisplayCategory */ null); + + IntentFilter intentFilter = new IntentFilter(Intent.ACTION_VIEW); + intentFilter.addDataScheme("mailto"); + + // register interceptor with different filter + mDeviceImpl.registerIntentInterceptor(interceptor, intentFilter); + + assertThat(gwpc.canActivityBeLaunched(activityInfos.get(0), intent, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID, /*isNewTask=*/false)) + .isTrue(); + } + @Test public void nonRestrictedActivityOnRestrictedVirtualDisplay_startBlockedAlertActivity() { Intent blockedAppIntent = createRestrictedActivityBlockedIntent(List.of("abc"), diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java index aaa13512af251..c27043565fcca 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java @@ -85,6 +85,7 @@ public class VirtualAudioControllerTest { /* pipBlockedCallback= */ null, /* activityBlockedCallback= */ null, /* secureWindowCallback= */ null, + /* intentListenerCallback= */ null, /* displayCategories= */ new ArrayList<>(), /* recentsPolicy= */ VirtualDeviceParams.RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java index 2ce1d6055d86e..3c5bc17049543 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.mock; import android.app.WindowConfiguration; import android.content.ComponentName; +import android.content.Intent; import android.content.pm.ActivityInfo; import android.os.UserHandle; import android.util.ArraySet; @@ -227,7 +228,7 @@ public class DisplayWindowPolicyControllerTests extends WindowTestsBase { ArraySet mRunningUids = new ArraySet<>(); @Override - public boolean canActivityBeLaunched(@NonNull ActivityInfo activity, + public boolean canActivityBeLaunched(@NonNull ActivityInfo activity, Intent intent, @WindowConfiguration.WindowingMode int windowingMode, int launchingFromDisplayId, boolean isNewTask) { return false;