diff options
2 files changed, 167 insertions, 26 deletions
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 5c8fb2ecec0f..95b9e58a9dfd 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -45,10 +45,12 @@ import android.hardware.input.VirtualMouseScrollEvent; import android.hardware.input.VirtualTouchEvent; import android.os.Binder; import android.os.IBinder; +import android.os.PowerManager; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.UserHandle; import android.os.UserManager; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; @@ -59,6 +61,7 @@ import com.android.server.LocalServices; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.Map; import java.util.Set; @@ -78,6 +81,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private final OnDeviceCloseListener mListener; private final IBinder mAppToken; private final VirtualDeviceParams mParams; + private final Map<Integer, PowerManager.WakeLock> mPerDisplayWakelocks = new ArrayMap<>(); private final IVirtualDeviceActivityListener mActivityListener; private ActivityListener createListenerAdapter(int displayId) { @@ -206,6 +210,16 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @Override // Binder call public void close() { + synchronized (mVirtualDeviceLock) { + if (!mPerDisplayWakelocks.isEmpty()) { + mPerDisplayWakelocks.forEach((displayId, wakeLock) -> { + Slog.w(TAG, "VirtualDisplay " + displayId + " owned by UID " + mOwnerUid + + " was not properly released"); + wakeLock.release(); + }); + mPerDisplayWakelocks.clear(); + } + } mListener.onClose(mAssociationInfo.getId()); mAppToken.unlinkToDeath(this, 0); mInputController.close(); @@ -383,22 +397,48 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } DisplayWindowPolicyController onVirtualDisplayCreatedLocked(int displayId) { - if (mVirtualDisplayIds.contains(displayId)) { - throw new IllegalStateException( - "Virtual device already have a virtual display with ID " + displayId); + synchronized (mVirtualDeviceLock) { + if (mVirtualDisplayIds.contains(displayId)) { + throw new IllegalStateException( + "Virtual device already have a virtual display with ID " + displayId); + } + mVirtualDisplayIds.add(displayId); + + // Since we're being called in the middle of the display being created, we post a + // task to grab the wakelock instead of doing it synchronously here, to avoid + // reentrancy problems. + mContext.getMainThreadHandler().post(() -> addWakeLockForDisplay(displayId)); + + LocalServices.getService( + InputManagerInternal.class).setDisplayEligibilityForPointerCapture(displayId, + false); + final GenericWindowPolicyController dwpc = + new GenericWindowPolicyController(FLAG_SECURE, + SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, + getAllowedUserHandles(), + mParams.getAllowedActivities(), + mParams.getBlockedActivities(), + createListenerAdapter(displayId)); + mWindowPolicyControllers.put(displayId, dwpc); + return dwpc; + } + } + + void addWakeLockForDisplay(int displayId) { + synchronized (mVirtualDeviceLock) { + if (!mVirtualDisplayIds.contains(displayId) + || mPerDisplayWakelocks.containsKey(displayId)) { + Slog.e(TAG, "Not creating wakelock for displayId " + displayId); + return; + } + PowerManager powerManager = mContext.getSystemService(PowerManager.class); + PowerManager.WakeLock wakeLock = powerManager.newWakeLock( + PowerManager.SCREEN_BRIGHT_WAKE_LOCK + | PowerManager.ACQUIRE_CAUSES_WAKEUP, + TAG + ":" + displayId, displayId); + wakeLock.acquire(); + mPerDisplayWakelocks.put(displayId, wakeLock); } - mVirtualDisplayIds.add(displayId); - LocalServices.getService(InputManagerInternal.class).setDisplayEligibilityForPointerCapture( - displayId, false); - final GenericWindowPolicyController dwpc = - new GenericWindowPolicyController(FLAG_SECURE, - SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, - getAllowedUserHandles(), - mParams.getAllowedActivities(), - mParams.getBlockedActivities(), - createListenerAdapter(displayId)); - mWindowPolicyControllers.put(displayId, dwpc); - return dwpc; } private ArraySet<UserHandle> getAllowedUserHandles() { @@ -420,14 +460,22 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } void onVirtualDisplayRemovedLocked(int displayId) { - if (!mVirtualDisplayIds.contains(displayId)) { - throw new IllegalStateException( - "Virtual device doesn't have a virtual display with ID " + displayId); + synchronized (mVirtualDeviceLock) { + if (!mVirtualDisplayIds.contains(displayId)) { + throw new IllegalStateException( + "Virtual device doesn't have a virtual display with ID " + displayId); + } + PowerManager.WakeLock wakeLock = mPerDisplayWakelocks.get(displayId); + if (wakeLock != null) { + wakeLock.release(); + mPerDisplayWakelocks.remove(displayId); + } + mVirtualDisplayIds.remove(displayId); + LocalServices.getService( + InputManagerInternal.class).setDisplayEligibilityForPointerCapture( + displayId, true); + mWindowPolicyControllers.remove(displayId); } - mVirtualDisplayIds.remove(displayId); - LocalServices.getService(InputManagerInternal.class).setDisplayEligibilityForPointerCapture( - displayId, true); - mWindowPolicyControllers.remove(displayId); } int getOwnerUid() { 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 72100e44b3eb..e36263e247bf 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 @@ -18,17 +18,21 @@ package com.android.server.companion.virtual; import static com.google.common.truth.Truth.assertWithMessage; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertThrows; import android.Manifest; import android.app.admin.DevicePolicyManager; +import android.companion.AssociationInfo; import android.companion.virtual.IVirtualDeviceActivityListener; import android.companion.virtual.VirtualDeviceParams; import android.content.Context; @@ -41,24 +45,35 @@ import android.hardware.input.VirtualMouseButtonEvent; import android.hardware.input.VirtualMouseRelativeEvent; import android.hardware.input.VirtualMouseScrollEvent; import android.hardware.input.VirtualTouchEvent; +import android.net.MacAddress; import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.IPowerManager; +import android.os.IThermalService; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.WorkSource; import android.platform.test.annotations.Presubmit; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; import android.view.KeyEvent; import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; import com.android.server.LocalServices; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @Presubmit -@RunWith(AndroidJUnit4.class) +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) public class VirtualDeviceManagerServiceTest { private static final String DEVICE_NAME = "device name"; @@ -84,6 +99,11 @@ public class VirtualDeviceManagerServiceTest { private InputManagerInternal mInputManagerInternalMock; @Mock private IVirtualDeviceActivityListener mActivityListener; + @Mock + IPowerManager mIPowerManagerMock; + @Mock + IThermalService mIThermalServiceMock; + private PowerManager mPowerManager; @Before public void setUp() { @@ -102,10 +122,17 @@ public class VirtualDeviceManagerServiceTest { when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn( mDevicePolicyManagerMock); + mPowerManager = new PowerManager(mContext, mIPowerManagerMock, mIThermalServiceMock, + new Handler(TestableLooper.get(this).getLooper())); + when(mContext.getSystemService(Context.POWER_SERVICE)).thenReturn(mPowerManager); + mInputController = new InputController(new Object(), mNativeWrapperMock); + AssociationInfo associationInfo = new AssociationInfo(1, 0, null, + MacAddress.BROADCAST_ADDRESS, "", null, true, false, 0, 0); mDeviceImpl = new VirtualDeviceImpl(mContext, - /* association info */ null, new Binder(), /* uid */ 0, mInputController, - (int associationId) -> {}, mPendingTrampolineCallback, mActivityListener, + associationInfo, new Binder(), /* uid */ 0, mInputController, + (int associationId) -> { + }, mPendingTrampolineCallback, mActivityListener, new VirtualDeviceParams.Builder().build()); } @@ -118,6 +145,72 @@ public class VirtualDeviceManagerServiceTest { } @Test + public void onVirtualDisplayCreatedLocked_wakeLockIsAcquired() throws RemoteException { + final int displayId = 2; + mDeviceImpl.onVirtualDisplayCreatedLocked(displayId); + verify(mIPowerManagerMock, never()).acquireWakeLock(any(Binder.class), anyInt(), + nullable(String.class), nullable(String.class), nullable(WorkSource.class), + nullable(String.class), anyInt()); + TestableLooper.get(this).processAllMessages(); + verify(mIPowerManagerMock, Mockito.times(1)).acquireWakeLock(any(Binder.class), anyInt(), + nullable(String.class), nullable(String.class), nullable(WorkSource.class), + nullable(String.class), eq(displayId)); + } + + @Test + public void onVirtualDisplayCreatedLocked_duplicateCalls_onlyOneWakeLockIsAcquired() + throws RemoteException { + final int displayId = 2; + mDeviceImpl.onVirtualDisplayCreatedLocked(displayId); + assertThrows(IllegalStateException.class, + () -> mDeviceImpl.onVirtualDisplayCreatedLocked(displayId)); + TestableLooper.get(this).processAllMessages(); + verify(mIPowerManagerMock, Mockito.times(1)).acquireWakeLock(any(Binder.class), anyInt(), + nullable(String.class), nullable(String.class), nullable(WorkSource.class), + nullable(String.class), eq(displayId)); + } + + @Test + public void onVirtualDisplayRemovedLocked_unknownDisplayId_throwsException() { + final int unknownDisplayId = 999; + assertThrows(IllegalStateException.class, + () -> mDeviceImpl.onVirtualDisplayRemovedLocked(unknownDisplayId)); + } + + @Test + public void onVirtualDisplayRemovedLocked_wakeLockIsReleased() throws RemoteException { + final int displayId = 2; + mDeviceImpl.onVirtualDisplayCreatedLocked(displayId); + ArgumentCaptor<IBinder> wakeLockCaptor = ArgumentCaptor.forClass(IBinder.class); + TestableLooper.get(this).processAllMessages(); + verify(mIPowerManagerMock, Mockito.times(1)).acquireWakeLock(wakeLockCaptor.capture(), + anyInt(), + nullable(String.class), nullable(String.class), nullable(WorkSource.class), + nullable(String.class), eq(displayId)); + + IBinder wakeLock = wakeLockCaptor.getValue(); + mDeviceImpl.onVirtualDisplayRemovedLocked(displayId); + verify(mIPowerManagerMock, Mockito.times(1)).releaseWakeLock(eq(wakeLock), anyInt()); + } + + @Test + public void addVirtualDisplay_displayNotReleased_wakeLockIsReleased() throws RemoteException { + final int displayId = 2; + mDeviceImpl.onVirtualDisplayCreatedLocked(displayId); + ArgumentCaptor<IBinder> wakeLockCaptor = ArgumentCaptor.forClass(IBinder.class); + TestableLooper.get(this).processAllMessages(); + verify(mIPowerManagerMock, Mockito.times(1)).acquireWakeLock(wakeLockCaptor.capture(), + anyInt(), + nullable(String.class), nullable(String.class), nullable(WorkSource.class), + nullable(String.class), eq(displayId)); + IBinder wakeLock = wakeLockCaptor.getValue(); + + // Close the VirtualDevice without first notifying it of the VirtualDisplay removal. + mDeviceImpl.close(); + verify(mIPowerManagerMock, Mockito.times(1)).releaseWakeLock(eq(wakeLock), anyInt()); + } + + @Test public void createVirtualKeyboard_noDisplay_failsSecurityException() { assertThrows( SecurityException.class, |