blob: 5b784faf33f2916488a2ca8d7f8a10276f08d63a [file] [log] [blame]
/*
* Copyright (C) 2023 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.virtualdevice.cts;
import static android.Manifest.permission.ACTIVITY_EMBEDDING;
import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
import static android.Manifest.permission.CREATE_VIRTUAL_DEVICE;
import static android.Manifest.permission.READ_CLIPBOARD_IN_BACKGROUND;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.Manifest.permission.WAKE_LOCK;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_DEFAULT;
import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
import static android.virtualdevice.cts.common.ClipboardTestConstants.ACTION_GET_CLIP;
import static android.virtualdevice.cts.common.ClipboardTestConstants.ACTION_SET_AND_GET_CLIP;
import static android.virtualdevice.cts.common.ClipboardTestConstants.ACTION_SET_CLIP;
import static android.virtualdevice.cts.common.ClipboardTestConstants.ACTION_WAIT_FOR_CLIP;
import static android.virtualdevice.cts.common.ClipboardTestConstants.EXTRA_FINISH_AFTER_SENDING_RESULT;
import static android.virtualdevice.cts.common.ClipboardTestConstants.EXTRA_GET_CLIP_DATA;
import static android.virtualdevice.cts.common.ClipboardTestConstants.EXTRA_HAS_CLIP;
import static android.virtualdevice.cts.common.ClipboardTestConstants.EXTRA_NOTIFY_WHEN_ATTACHED_TO_WINDOW;
import static android.virtualdevice.cts.common.ClipboardTestConstants.EXTRA_NOT_FOCUSABLE;
import static android.virtualdevice.cts.common.ClipboardTestConstants.EXTRA_RESULT_RECEIVER;
import static android.virtualdevice.cts.common.ClipboardTestConstants.EXTRA_SET_CLIP_DATA;
import static android.virtualdevice.cts.common.ClipboardTestConstants.EXTRA_WAIT_FOR_FOCUS;
import static android.virtualdevice.cts.common.ClipboardTestConstants.RESULT_CODE_ATTACHED_TO_WINDOW;
import static android.virtualdevice.cts.common.ClipboardTestConstants.RESULT_CODE_CLIP_LISTENER_READY;
import static android.virtualdevice.cts.util.VirtualDeviceTestUtils.createActivityOptions;
import static android.virtualdevice.cts.util.VirtualDeviceTestUtils.createResultReceiver;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import android.app.Activity;
import android.app.UiAutomation;
import android.companion.virtual.VirtualDeviceManager;
import android.companion.virtual.VirtualDeviceManager.VirtualDevice;
import android.companion.virtual.VirtualDeviceParams;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.ImageReader;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.platform.test.annotations.AppModeFull;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.support.test.uiautomator.UiDevice;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.MotionEvent;
import android.virtualdevice.cts.util.FakeAssociationRule;
import android.virtualdevice.cts.util.VirtualDeviceTestUtils;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.compatibility.common.util.AdoptShellPermissionsRule;
import com.android.compatibility.common.util.SystemUtil;
import com.android.compatibility.common.util.Timeout;
import com.google.common.util.concurrent.SettableFuture;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
@AppModeFull(reason = "VirtualDeviceManager cannot be accessed by instant apps")
public class StreamedAppClipboardTest {
private static final String TAG = "StreamedAppClipboardTest";
private static final ComponentName CLIPBOARD_TEST_ACTIVITY =
new ComponentName("android.virtualdevice.streamedtestapp",
"android.virtualdevice.streamedtestapp.ClipboardTestActivity");
private static final ComponentName CLIPBOARD_TEST_ACTIVITY_2 =
new ComponentName("android.virtualdevice.streamedtestapp2",
"android.virtualdevice.streamedtestapp2.ClipboardTestActivity2");
private static final int EVENT_TIMEOUT_MS = 3000;
@Rule
public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
InstrumentationRegistry.getInstrumentation().getUiAutomation(),
ACTIVITY_EMBEDDING,
ADD_TRUSTED_DISPLAY,
CREATE_VIRTUAL_DEVICE,
READ_CLIPBOARD_IN_BACKGROUND,
READ_DEVICE_CONFIG,
WRITE_DEVICE_CONFIG,
WRITE_SECURE_SETTINGS,
WAKE_LOCK);
@Rule
public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule();
private VirtualDeviceManager mVirtualDeviceManager;
private VirtualDevice mVirtualDevice;
private VirtualDisplay mVirtualDisplay;
private ImageReader mImageReaderForVirtualDisplay;
private Context mContext;
@Mock
private VirtualDisplay.Callback mVirtualDisplayCallback;
@Mock
private VirtualDeviceTestUtils.OnReceiveResultListener mOnReceiveResultListener;
private ResultReceiver mResultReceiver;
// Used to keep track of the overridden DeviceConfig value for VirtualDevice clipboard silos,
// so we can restore it after the test.
private String mOriginalVDSilosDeviceFlagValue;
@BeforeClass
public static void setUpClass() {
// Disable Toasts shown on clipboard access to avoid a crash in sysui that happens when a
// VirtualDisplay with a Toast about to be shown is closed. (b/265325338)
SystemUtil.runWithShellPermissionIdentity(()-> {
Context context = InstrumentationRegistry.getInstrumentation().getContext();
Settings.Secure.putInt(context.getContentResolver(),
Settings.Secure.CLIPBOARD_SHOW_ACCESS_NOTIFICATIONS, 0);
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).waitForIdle();
}, WRITE_SECURE_SETTINGS);
}
@AfterClass
public static void tearDownClass() {
SystemUtil.runWithShellPermissionIdentity(() -> {
Context context = InstrumentationRegistry.getInstrumentation().getContext();
Settings.Secure.resetToDefaults(context.getContentResolver(), null);
}, WRITE_SECURE_SETTINGS);
}
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mContext = getApplicationContext();
final PackageManager packageManager = mContext.getPackageManager();
assumeTrue(packageManager
.hasSystemFeature(PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS));
// TODO(b/261155110): Re-enable tests once freeform mode is supported in Virtual Display.
assumeFalse("Skipping test: VirtualDisplay window policy doesn't support freeform.",
packageManager.hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT));
mOriginalVDSilosDeviceFlagValue = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CLIPBOARD,
ClipboardManager.DEVICE_CONFIG_ALLOW_VIRTUALDEVICE_SILOS);
SettableFuture<Void> flagBecameSet = SettableFuture.create();
DeviceConfig.OnPropertiesChangedListener deviceConfigListener = (properties) -> {
if (properties.getBoolean(
ClipboardManager.DEVICE_CONFIG_ALLOW_VIRTUALDEVICE_SILOS, false)) {
flagBecameSet.set(null);
}
};
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_CLIPBOARD,
Executors.newSingleThreadExecutor(), deviceConfigListener);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_CLIPBOARD,
ClipboardManager.DEVICE_CONFIG_ALLOW_VIRTUALDEVICE_SILOS, "true",
/* makeDefault= */ false);
// Wait for the DeviceConfig change to be broadcasted, so we can be sure that
// ClipboardService is using the updated value.
flagBecameSet.get(EVENT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
DeviceConfig.removeOnPropertiesChangedListener(deviceConfigListener);
mVirtualDeviceManager = mContext.getSystemService(VirtualDeviceManager.class);
mVirtualDevice =
mVirtualDeviceManager.createVirtualDevice(
mFakeAssociationRule.getAssociationInfo().getId(),
new VirtualDeviceParams.Builder().build());
mImageReaderForVirtualDisplay = ImageReader.newInstance(/* width= */ 100, /* height= */ 100,
PixelFormat.RGBA_8888, /* maxImages= */ 1);
mVirtualDisplay = mVirtualDevice.createVirtualDisplay(
/* width= */ 100,
/* height= */ 100,
/* densityDpi= */ 240,
/* surface= */ mImageReaderForVirtualDisplay.getSurface(),
/* flags= */ 0,
/* executor= */ Runnable::run,
mVirtualDisplayCallback);
mResultReceiver = createResultReceiver(mOnReceiveResultListener);
}
@After
public void tearDown() {
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_CLIPBOARD,
ClipboardManager.DEVICE_CONFIG_ALLOW_VIRTUALDEVICE_SILOS,
mOriginalVDSilosDeviceFlagValue, /* makeDefault= */ false);
if (mVirtualDisplay != null) {
mVirtualDisplay.release();
}
if (mVirtualDevice != null) {
mVirtualDevice.close();
}
if (mImageReaderForVirtualDisplay != null) {
mImageReaderForVirtualDisplay.close();
}
}
@Test
public void oneAppOnVirtualDevice_canWriteAndReadClipboard() {
ClipboardManager clipboard = mContext.createDeviceContext(
DEVICE_ID_DEFAULT).getSystemService(ClipboardManager.class);
clipboard.clearPrimaryClip();
assertThat(clipboard.hasPrimaryClip()).isFalse();
ClipData clipToSet = ClipData.newPlainText("some label", "Hello World");
final Intent intent = new Intent(ACTION_SET_AND_GET_CLIP)
.setComponent(CLIPBOARD_TEST_ACTIVITY)
.putExtra(EXTRA_SET_CLIP_DATA, clipToSet)
.putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver);
launchAndAwaitActivityOnVirtualDisplay(intent);
tapOnDisplay(mVirtualDisplay.getDisplay());
ArgumentCaptor<Bundle> bundle = ArgumentCaptor.forClass(Bundle.class);
verify(mOnReceiveResultListener, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(anyInt(),
bundle.capture());
// Make sure the activity was able to read what it wrote itself.
ClipData clipData = bundle.getValue().getParcelable(EXTRA_GET_CLIP_DATA, ClipData.class);
assertThat(clipData).isNotNull();
assertThat(clipData.getItemCount()).isEqualTo(1);
assertThat(clipData.getDescription().getLabel().toString()).isEqualTo(
clipToSet.getDescription().getLabel().toString());
assertThat(clipData.getItemAt(0).getText()).isEqualTo(clipToSet.getItemAt(0).getText());
// We shouldn't observe what was written to the VirtualDevice siloed clipboard
assertThat(clipboard.hasPrimaryClip()).isFalse();
}
@Test
public void oneAppOnVirtualDevice_appWritesClipboard_deviceOwnerGetsEventAndCanRead() {
ClipboardManager deviceClipboard = mVirtualDevice.createContext().getSystemService(
ClipboardManager.class);
ClipboardManager.OnPrimaryClipChangedListener deviceClipboardListener =
mock(ClipboardManager.OnPrimaryClipChangedListener.class);
deviceClipboard.addPrimaryClipChangedListener(deviceClipboardListener);
ClipData clipToSet = ClipData.newPlainText("some label", "Hello World");
final Intent intent = new Intent(ACTION_SET_CLIP)
.setComponent(CLIPBOARD_TEST_ACTIVITY)
.putExtra(EXTRA_SET_CLIP_DATA, clipToSet);
launchAndAwaitActivityOnVirtualDisplay(intent);
tapOnDisplay(mVirtualDisplay.getDisplay());
// We use atLeastOnce for onPrimaryClipChanged because it can fire more than once as
// text classification results on the clipboard content become available.
verify(deviceClipboardListener,
timeout(EVENT_TIMEOUT_MS).atLeastOnce()).onPrimaryClipChanged();
assertThat(deviceClipboard.hasPrimaryClip()).isTrue();
ClipData clipData = deviceClipboard.getPrimaryClip();
assertThat(clipData.getItemCount()).isEqualTo(1);
assertThat(clipData.getDescription().getLabel().toString()).isEqualTo(
clipToSet.getDescription().getLabel().toString());
assertThat(clipData.getItemAt(0).getText()).isEqualTo(clipToSet.getItemAt(0).getText());
}
@Test
public void oneAppOnVirtualDevice_deviceOwnerWritesClipboard_appGetsEventAndCanRead() {
ClipboardManager deviceClipboard = mVirtualDevice.createContext().getSystemService(
ClipboardManager.class);
launchAndAwaitActivityOnVirtualDisplay(new Intent(ACTION_WAIT_FOR_CLIP)
.setComponent(CLIPBOARD_TEST_ACTIVITY)
.putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver));
// Give the activity focus so that it is allowed to read the clipboard.
tapOnDisplay(mVirtualDisplay.getDisplay());
// Make sure the clip listener is ready
verify(mOnReceiveResultListener, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(
eq(RESULT_CODE_CLIP_LISTENER_READY), any());
ClipData clipToSet = ClipData.newPlainText("some label", "Hello World");
deviceClipboard.setPrimaryClip(clipToSet);
ArgumentCaptor<Bundle> bundle = ArgumentCaptor.forClass(Bundle.class);
verify(mOnReceiveResultListener, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(
eq(Activity.RESULT_OK), bundle.capture());
ClipData clipData = bundle.getValue().getParcelable(EXTRA_GET_CLIP_DATA, ClipData.class);
assertThat(clipData).isNotNull();
assertThat(clipData.getItemCount()).isEqualTo(1);
assertThat(clipData.getDescription().getLabel().toString()).isEqualTo(
clipToSet.getDescription().getLabel().toString());
assertThat(clipData.getItemAt(0).getText()).isEqualTo(clipToSet.getItemAt(0).getText());
}
@Test
public void unfocusedAppOnVirtualDevice_deviceOwnerWritesClipboard_appCannotRead() {
ClipboardManager deviceClipboard = mVirtualDevice.createContext().getSystemService(
ClipboardManager.class);
ClipData clipToSet = ClipData.newPlainText("some label", "Hello World");
deviceClipboard.setPrimaryClip(clipToSet);
launchAndAwaitActivityOnVirtualDisplay(new Intent(ACTION_GET_CLIP)
.setComponent(CLIPBOARD_TEST_ACTIVITY)
.putExtra(EXTRA_NOT_FOCUSABLE, true)
.putExtra(EXTRA_WAIT_FOR_FOCUS, false)
.putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver));
// Note that we do not tap on the display here - the app should remain unfocused and should
// not be able to read the contents of the clipboard.
ArgumentCaptor<Bundle> bundle = ArgumentCaptor.forClass(Bundle.class);
verify(mOnReceiveResultListener, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(anyInt(),
bundle.capture());
assertThat(bundle.getValue().getBoolean(EXTRA_HAS_CLIP, true)).isFalse();
ClipData clipData = bundle.getValue().getParcelable(EXTRA_GET_CLIP_DATA, ClipData.class);
assertThat(clipData).isNull();
}
@Test
public void oneAppOnVirtualDeviceOneOnDefault_firstAppWrites_secondCannotRead() {
ClipboardManager defaultClipboard = mContext.createDeviceContext(
DEVICE_ID_DEFAULT).getSystemService(ClipboardManager.class);
defaultClipboard.clearPrimaryClip();
final Intent firstAppIntent =
new Intent(ACTION_SET_CLIP).setComponent(CLIPBOARD_TEST_ACTIVITY);
ClipData clipToSet = ClipData.newPlainText("some label", "Hello World");
firstAppIntent.putExtra(EXTRA_SET_CLIP_DATA, clipToSet);
firstAppIntent.putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver);
launchAndAwaitActivityOnVirtualDisplay(firstAppIntent);
tapOnDisplay(mVirtualDisplay.getDisplay());
verify(mOnReceiveResultListener, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(anyInt(),
any());
VirtualDeviceTestUtils.OnReceiveResultListener secondResultReceiver = mock(
VirtualDeviceTestUtils.OnReceiveResultListener.class);
final Intent secondAppIntent = new Intent(ACTION_GET_CLIP)
.setComponent(CLIPBOARD_TEST_ACTIVITY_2)
.putExtra(EXTRA_NOTIFY_WHEN_ATTACHED_TO_WINDOW, true)
.putExtra(EXTRA_RESULT_RECEIVER,
createResultReceiver(secondResultReceiver))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
InstrumentationRegistry.getInstrumentation().getTargetContext()
.startActivity(secondAppIntent, null);
verify(secondResultReceiver, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(
eq(RESULT_CODE_ATTACHED_TO_WINDOW), any());
DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
tapOnDisplay(displayManager.getDisplay(Display.DEFAULT_DISPLAY));
ArgumentCaptor<Bundle> bundle = ArgumentCaptor.forClass(Bundle.class);
verify(secondResultReceiver, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(
eq(Activity.RESULT_OK), bundle.capture());
assertThat(bundle.getValue().getBoolean(EXTRA_HAS_CLIP, true)).isFalse();
ClipData clipData = bundle.getValue().getParcelable(EXTRA_GET_CLIP_DATA, ClipData.class);
assertThat(clipData).isNull();
}
@Test
public void twoAppsOnVirtualDevice_firstAppWrites_secondAppCanRead() {
final Intent firstAppIntent =
new Intent(ACTION_SET_CLIP).setComponent(CLIPBOARD_TEST_ACTIVITY);
ClipData clipToSet = ClipData.newPlainText("some label", "Hello World");
firstAppIntent.putExtra(EXTRA_SET_CLIP_DATA, clipToSet);
firstAppIntent.putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver);
launchAndAwaitActivityOnVirtualDisplay(firstAppIntent);
tapOnDisplay(mVirtualDisplay.getDisplay());
verify(mOnReceiveResultListener, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(anyInt(),
any());
VirtualDeviceTestUtils.OnReceiveResultListener secondResultReceiver = mock(
VirtualDeviceTestUtils.OnReceiveResultListener.class);
final Intent secondAppIntent = new Intent(ACTION_GET_CLIP).setComponent(
CLIPBOARD_TEST_ACTIVITY_2)
.putExtra(EXTRA_RESULT_RECEIVER, createResultReceiver(secondResultReceiver));
launchAndAwaitActivityOnVirtualDisplay(secondAppIntent);
tapOnDisplay(mVirtualDisplay.getDisplay());
// Make sure that the second activity now running on top of the VirtualDisplay reads the
// value which was set by the first activity.
ArgumentCaptor<Bundle> bundle = ArgumentCaptor.forClass(Bundle.class);
verify(secondResultReceiver, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(anyInt(),
bundle.capture());
ClipData clipData = bundle.getValue().getParcelable(EXTRA_GET_CLIP_DATA, ClipData.class);
assertThat(clipData).isNotNull();
assertThat(clipData.getItemCount()).isEqualTo(1);
assertThat(clipData.getDescription().getLabel().toString()).isEqualTo(
clipToSet.getDescription().getLabel().toString());
assertThat(clipData.getItemAt(0).getText()).isEqualTo(clipToSet.getItemAt(0).getText());
}
@Test
public void twoAppsOnVirtualDevice_bothAppsWriteInSequence_deviceOwnerCanObserveBoth() {
ClipboardManager deviceClipboard = mVirtualDevice.createContext().getSystemService(
ClipboardManager.class);
ClipboardManager.OnPrimaryClipChangedListener deviceClipboardListener =
mock(ClipboardManager.OnPrimaryClipChangedListener.class);
deviceClipboard.addPrimaryClipChangedListener(deviceClipboardListener);
ClipData app1Clip = ClipData.newPlainText("app one", "Hello World 1");
final Intent firstAppIntent =
new Intent(ACTION_SET_CLIP).setComponent(CLIPBOARD_TEST_ACTIVITY)
.putExtra(EXTRA_SET_CLIP_DATA, app1Clip)
.putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver);
launchAndAwaitActivityOnVirtualDisplay(firstAppIntent);
tapOnDisplay(mVirtualDisplay.getDisplay());
verify(mOnReceiveResultListener, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(anyInt(),
any());
verify(deviceClipboardListener,
timeout(EVENT_TIMEOUT_MS).atLeastOnce()).onPrimaryClipChanged();
assertThat(deviceClipboard.hasPrimaryClip()).isTrue();
ClipData readClip1 = deviceClipboard.getPrimaryClip();
assertThat(readClip1).isNotNull();
assertThat(readClip1.getItemCount()).isEqualTo(1);
assertThat(readClip1.getDescription().getLabel().toString()).isEqualTo(
app1Clip.getDescription().getLabel().toString());
assertThat(readClip1.getItemAt(0).getText()).isEqualTo(app1Clip.getItemAt(0).getText());
ClipData app2Clip = ClipData.newPlainText("app two", "Hello World 2");
VirtualDeviceTestUtils.OnReceiveResultListener secondResultReceiver = mock(
VirtualDeviceTestUtils.OnReceiveResultListener.class);
final Intent secondAppIntent = new Intent(ACTION_SET_CLIP).setComponent(
CLIPBOARD_TEST_ACTIVITY_2)
.putExtra(EXTRA_SET_CLIP_DATA, app2Clip)
.putExtra(EXTRA_RESULT_RECEIVER, createResultReceiver(secondResultReceiver));
launchAndAwaitActivityOnVirtualDisplay(secondAppIntent);
reset(deviceClipboardListener);
tapOnDisplay(mVirtualDisplay.getDisplay());
verify(secondResultReceiver, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(anyInt(), any());
verify(deviceClipboardListener,
timeout(EVENT_TIMEOUT_MS).atLeastOnce()).onPrimaryClipChanged();
assertThat(deviceClipboard.hasPrimaryClip()).isTrue();
ClipData readClip2 = deviceClipboard.getPrimaryClip();
assertThat(readClip2).isNotNull();
assertThat(readClip2.getItemCount()).isEqualTo(1);
assertThat(readClip2.getDescription().getLabel().toString()).isEqualTo(
app2Clip.getDescription().getLabel().toString());
assertThat(readClip2.getItemAt(0).getText()).isEqualTo(app2Clip.getItemAt(0).getText());
}
@Test
public void twoAppsOnVirtualDevice_focusedAppWrites_unfocusedAppDoesNotGetEvent() {
// The first app does not wait for focus, and immediately starts waiting for a clipboard
// change event.
launchAndAwaitActivityOnVirtualDisplay(new Intent(ACTION_WAIT_FOR_CLIP)
.setComponent(CLIPBOARD_TEST_ACTIVITY)
.putExtra(EXTRA_NOT_FOCUSABLE, true)
.putExtra(EXTRA_WAIT_FOR_FOCUS, false)
.putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver));
// Make sure the clip listener is ready
verify(mOnReceiveResultListener, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(
eq(RESULT_CODE_CLIP_LISTENER_READY), any());
// The second app is launched on top of the first app, and once given focus it writes into
// the clipboard.
ClipData app2Clip = ClipData.newPlainText("app two", "Hello World 2");
VirtualDeviceTestUtils.OnReceiveResultListener secondResultReceiver = mock(
VirtualDeviceTestUtils.OnReceiveResultListener.class);
final Intent secondAppIntent = new Intent(ACTION_SET_CLIP)
.setComponent(CLIPBOARD_TEST_ACTIVITY_2)
.putExtra(EXTRA_SET_CLIP_DATA, app2Clip)
.putExtra(EXTRA_RESULT_RECEIVER, createResultReceiver(secondResultReceiver));
launchAndAwaitActivityOnVirtualDisplay(secondAppIntent);
tapOnDisplay(mVirtualDisplay.getDisplay());
// The second app should have been successful at writing the clipboard, and the first app
// should not have gotten any change events that caused it to send a result back.
verify(secondResultReceiver, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(anyInt(), any());
verify(mOnReceiveResultListener, never()).onReceiveResult(eq(Activity.RESULT_OK), any());
}
@Test
public void appRunsOnTwoVirtualDevices_clipboardWritesAreIsolated() {
VirtualDevice secondDevice = mVirtualDeviceManager.createVirtualDevice(
mFakeAssociationRule.getAssociationInfo().getId(),
new VirtualDeviceParams.Builder().build());
ImageReader reader = ImageReader.newInstance(/* width= */ 100, /* height= */ 100,
PixelFormat.RGBA_8888, /* maxImages= */ 1);
VirtualDisplay secondVirtualDisplay = secondDevice.createVirtualDisplay(
/* width= */ 100,
/* height= */ 100,
/* densityDpi= */ 240,
/* surface= */ reader.getSurface(),
/* flags= */ 0,
Runnable::run,
/* callback= */ null);
try {
ClipboardManager deviceClipboard = mVirtualDevice.createContext().getSystemService(
ClipboardManager.class);
ClipboardManager secondDeviceClipboard = secondDevice.createContext().getSystemService(
ClipboardManager.class);
ClipboardManager.OnPrimaryClipChangedListener deviceClipboardListener =
mock(ClipboardManager.OnPrimaryClipChangedListener.class);
ClipboardManager.OnPrimaryClipChangedListener secondDeviceClipboardListener =
mock(ClipboardManager.OnPrimaryClipChangedListener.class);
deviceClipboard.addPrimaryClipChangedListener(deviceClipboardListener);
secondDeviceClipboard.addPrimaryClipChangedListener(secondDeviceClipboardListener);
ClipboardManager defaultClipboard = mContext.createDeviceContext(
DEVICE_ID_DEFAULT).getSystemService(ClipboardManager.class);
defaultClipboard.clearPrimaryClip();
ClipboardManager.OnPrimaryClipChangedListener defaultClipboardListener =
mock(ClipboardManager.OnPrimaryClipChangedListener.class);
defaultClipboard.addPrimaryClipChangedListener(defaultClipboardListener);
assertThat(deviceClipboard.hasPrimaryClip()).isFalse();
assertThat(secondDeviceClipboard.hasPrimaryClip()).isFalse();
assertThat(defaultClipboard.hasPrimaryClip()).isFalse();
ClipData clipData1 = ClipData.newPlainText("device one", "Hello World 1");
final Intent virtualDeviceOneIntent = new Intent(ACTION_SET_AND_GET_CLIP)
.setComponent(CLIPBOARD_TEST_ACTIVITY)
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
.putExtra(EXTRA_SET_CLIP_DATA,
clipData1)
.putExtra(EXTRA_FINISH_AFTER_SENDING_RESULT, false)
.putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver);
VirtualDeviceTestUtils.OnReceiveResultListener secondResultReceiver = mock(
VirtualDeviceTestUtils.OnReceiveResultListener.class);
ClipData clipData2 = ClipData.newPlainText("device two", "Hello World 2");
final Intent virtualDeviceTwoIntent = new Intent(ACTION_SET_AND_GET_CLIP)
.setComponent(CLIPBOARD_TEST_ACTIVITY)
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
.putExtra(EXTRA_SET_CLIP_DATA,
clipData2)
.putExtra(EXTRA_RESULT_RECEIVER,
createResultReceiver(secondResultReceiver));
launchAndAwaitActivityOnVirtualDisplay(virtualDeviceOneIntent);
launchAndAwaitActivity(virtualDeviceTwoIntent, secondDevice, secondVirtualDisplay);
// Focus the display on the first VirtualDevice
tapOnDisplay(mVirtualDisplay.getDisplay());
// Make sure app on first VirtualDevice could write and read its own clip
ArgumentCaptor<Bundle> bundle = ArgumentCaptor.forClass(Bundle.class);
verify(mOnReceiveResultListener, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(anyInt(),
bundle.capture());
assertThat(bundle.getValue().getBoolean(EXTRA_HAS_CLIP, false)).isTrue();
ClipData readClip =
bundle.getValue().getParcelable(EXTRA_GET_CLIP_DATA, ClipData.class);
assertThat(readClip.getItemCount()).isEqualTo(1);
assertThat(readClip.getDescription().getLabel().toString()).isEqualTo(
clipData1.getDescription().getLabel().toString());
assertThat(readClip.getItemAt(0).getText()).isEqualTo(
clipData1.getItemAt(0).getText());
// The second app should not have sent any result yet
verify(secondResultReceiver, never()).onReceiveResult(anyInt(), any());
// Make sure the device owner (us) read the correct clip from the first VirtualDevice,
// but hasn't gotten a clipboard change event for the second VirtualDevice's clipboard
verify(deviceClipboardListener,
timeout(EVENT_TIMEOUT_MS).atLeastOnce()).onPrimaryClipChanged();
verify(secondDeviceClipboardListener, never()).onPrimaryClipChanged();
assertThat(deviceClipboard.hasPrimaryClip()).isTrue();
readClip = deviceClipboard.getPrimaryClip();
assertThat(readClip.getItemCount()).isEqualTo(1);
assertThat(readClip.getDescription().getLabel().toString()).isEqualTo(
clipData1.getDescription().getLabel().toString());
assertThat(readClip.getItemAt(0).getText()).isEqualTo(
clipData1.getItemAt(0).getText());
// Focus the other VirtualDevice
tapOnDisplay(secondVirtualDisplay.getDisplay());
// Now make sure app on second VirtualDevice could write and read its own clip
bundle = ArgumentCaptor.forClass(Bundle.class);
verify(secondResultReceiver, timeout(EVENT_TIMEOUT_MS)).onReceiveResult(anyInt(),
bundle.capture());
assertThat(bundle.getValue().getBoolean(EXTRA_HAS_CLIP, false)).isTrue();
readClip = bundle.getValue().getParcelable(EXTRA_GET_CLIP_DATA, ClipData.class);
assertThat(readClip.getItemCount()).isEqualTo(1);
assertThat(readClip.getDescription().getLabel().toString()).isEqualTo(
clipData2.getDescription().getLabel().toString());
assertThat(readClip.getItemAt(0).getText()).isEqualTo(
clipData2.getItemAt(0).getText());
// Make sure the device owner (us) read the same clip from the second VirtualDevice
verify(secondDeviceClipboardListener,
timeout(EVENT_TIMEOUT_MS).atLeastOnce()).onPrimaryClipChanged();
assertThat(secondDeviceClipboard.hasPrimaryClip()).isTrue();
readClip = secondDeviceClipboard.getPrimaryClip();
assertThat(readClip.getItemCount()).isEqualTo(1);
assertThat(readClip.getDescription().getLabel().toString()).isEqualTo(
clipData2.getDescription().getLabel().toString());
assertThat(readClip.getItemAt(0).getText()).isEqualTo(
clipData2.getItemAt(0).getText());
assertThat(defaultClipboard.hasPrimaryClip()).isFalse();
verify(defaultClipboardListener, never()).onPrimaryClipChanged();
} finally {
secondVirtualDisplay.release();
secondDevice.close();
reader.close();
}
}
/**
* Launches an activity on mVirtualDisplay and waits for it to be running.
*/
private void launchAndAwaitActivityOnVirtualDisplay(Intent intent) {
launchAndAwaitActivity(intent, mVirtualDevice, mVirtualDisplay);
}
/**
* Launches an activity and waits for it to be running on top of the given VirtualDisplay
* owned by the given VirtualDevice.
*/
private void launchAndAwaitActivity(Intent intent, VirtualDevice virtualDevice,
VirtualDisplay targetDisplay) {
SettableFuture<Void> activityRunning = SettableFuture.create();
VirtualDeviceManager.ActivityListener activityListener =
new VirtualDeviceManager.ActivityListener() {
@Override
public void onTopActivityChanged(int displayId,
@NonNull ComponentName topActivity) {
if (topActivity.equals(intent.getComponent())) {
activityRunning.set(null);
}
}
@Override
public void onDisplayEmpty(int displayId) {
}
};
virtualDevice.addActivityListener(Runnable::run, activityListener);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
InstrumentationRegistry.getInstrumentation().getTargetContext()
.startActivity(intent, createActivityOptions(targetDisplay));
try {
activityRunning.get(EVENT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (Exception e) {
throw new RuntimeException(e);
}
virtualDevice.removeActivityListener(activityListener);
}
/**
* Tap in the center of the given Display, to give focus to the top activity there.
*/
private void tapOnDisplay(Display display) {
DisplayMetrics displayMetrics = new DisplayMetrics();
display.getRealMetrics(displayMetrics);
final Point p = new Point(displayMetrics.widthPixels / 2, displayMetrics.heightPixels / 2);
UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
// Sometimes the top activity on the display isn't quite ready to receive inputs and the
// injected input event gets rejected, so we retry a few times before giving up.
Timeout timeout = new Timeout("tapOnDisplay", EVENT_TIMEOUT_MS, 2f, EVENT_TIMEOUT_MS);
try {
timeout.run("tap on display " + display.getDisplayId(), ()-> {
final long downTime = SystemClock.elapsedRealtime();
final MotionEvent downEvent = MotionEvent.obtain(downTime, downTime,
MotionEvent.ACTION_DOWN, p.x, p.y, 0 /* metaState */);
downEvent.setDisplayId(display.getDisplayId());
boolean downEventSuccess =
uiAutomation.injectInputEvent(downEvent, /* sync */ true);
boolean upEventSuccess = false;
if (downEventSuccess) {
final long upTime = SystemClock.elapsedRealtime();
final MotionEvent upEvent = MotionEvent.obtain(downTime, upTime,
MotionEvent.ACTION_UP, p.x, p.y, 0 /* metaState */);
upEvent.setDisplayId(display.getDisplayId());
upEventSuccess = uiAutomation.injectInputEvent(upEvent, /* sync */ true);
}
return (downEventSuccess && upEventSuccess) ? Boolean.TRUE : null;
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}