Loading services/companion/java/com/android/server/companion/virtual/computercontrol/ComputerControlSessionImpl.java +23 −2 Original line number Diff line number Diff line Loading @@ -57,10 +57,12 @@ import java.util.concurrent.atomic.AtomicInteger; * A computer control session that encapsulates a {@link IVirtualDevice}. The device is created and * managed by the system, but it is still owned by the caller. */ final class ComputerControlSessionImpl extends IComputerControlSession.Stub { final class ComputerControlSessionImpl extends IComputerControlSession.Stub implements IBinder.DeathRecipient { private final IBinder mAppToken; private final ComputerControlSessionParams mParams; private final OnClosedListener mOnClosedListener; private final IVirtualDevice mVirtualDevice; private final int mVirtualDisplayId; private final IVirtualInputDevice mVirtualTouchscreen; Loading @@ -70,9 +72,11 @@ final class ComputerControlSessionImpl extends IComputerControlSession.Stub { ComputerControlSessionImpl(IBinder appToken, ComputerControlSessionParams params, AttributionSource attributionSource, PackageManager packageManager, ComputerControlSessionProcessor.VirtualDeviceFactory virtualDeviceFactory) { ComputerControlSessionProcessor.VirtualDeviceFactory virtualDeviceFactory, OnClosedListener onClosedListener) { mAppToken = appToken; mParams = params; mOnClosedListener = onClosedListener; VirtualDeviceParams virtualDeviceParams = new VirtualDeviceParams.Builder() .setName(mParams.name) .setDevicePolicy(VirtualDeviceParams.POLICY_TYPE_RECENTS, Loading Loading @@ -147,6 +151,7 @@ final class ComputerControlSessionImpl extends IComputerControlSession.Stub { mVirtualTouchscreen = mVirtualDevice.createVirtualTouchscreen( virtualTouchscreenConfig, new Binder(touchscreenName)); mAppToken.linkToDeath(this, 0); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } Loading Loading @@ -196,6 +201,17 @@ final class ComputerControlSessionImpl extends IComputerControlSession.Stub { @Override public void close() throws RemoteException { mVirtualDevice.close(); mAppToken.unlinkToDeath(this, 0); mOnClosedListener.onClosed(asBinder()); } @Override public void binderDied() { try { close(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } private static class ComputerControlActivityListener Loading @@ -218,4 +234,9 @@ final class ComputerControlSessionImpl extends IComputerControlSession.Stub { @Override public void onSecureWindowHidden(int displayId) {} } /** Interface for listening for closing of sessions. */ interface OnClosedListener { void onClosed(IBinder token); } } services/companion/java/com/android/server/companion/virtual/computercontrol/ComputerControlSessionProcessor.java +29 −3 Original line number Diff line number Diff line Loading @@ -26,11 +26,19 @@ import android.content.AttributionSource; import android.content.Context; import android.content.pm.PackageManager; import android.os.IBinder; import android.util.ArraySet; import com.android.internal.annotations.VisibleForTesting; public class ComputerControlSessionProcessor { // TODO(b/419548594): Make this configurable. @VisibleForTesting static final int MAXIMUM_CONCURRENT_SESSIONS = 5; private final PackageManager mPackageManager; private final VirtualDeviceFactory mVirtualDeviceFactory; private final ArraySet<IBinder> mSessions = new ArraySet<>(); public ComputerControlSessionProcessor( Context context, VirtualDeviceFactory virtualDeviceFactory) { Loading @@ -46,9 +54,27 @@ public class ComputerControlSessionProcessor { @NonNull AttributionSource attributionSource, @NonNull ComputerControlSessionParams params) { // TODO(b/430259551, b/432678191): Async creation of sessions triggering a consent dialog // TODO(b/419548594): Limit the number of active sessions return new ComputerControlSessionImpl( token, params, attributionSource, mPackageManager, mVirtualDeviceFactory); synchronized (mSessions) { if (mSessions.size() >= MAXIMUM_CONCURRENT_SESSIONS) { // TODO(b/419548594): Communicate this via a callback in an async flow. Returning // null is not good enough and the developer did nothing wrong, so we shouldn't // throw. throw new UnsupportedOperationException( "Maximum number of concurrent session reached, try again later."); } IComputerControlSession session = new ComputerControlSessionImpl( token, params, attributionSource, mPackageManager, mVirtualDeviceFactory, this::onSessionClosed); mSessions.add(session.asBinder()); return session; } } private void onSessionClosed(IBinder token) { synchronized (mSessions) { mSessions.remove(token); } } /** Loading services/tests/servicestests/src/com/android/server/companion/virtual/computercontrol/ComputerControlSessionProcessorTest.java 0 → 100644 +103 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.server.companion.virtual.computercontrol; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.testng.Assert.assertThrows; import android.companion.virtual.IVirtualDevice; import android.companion.virtual.computercontrol.ComputerControlSessionParams; import android.companion.virtual.computercontrol.IComputerControlSession; import android.content.AttributionSource; import android.content.Context; import android.os.Binder; import android.view.Surface; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; @RunWith(AndroidJUnit4.class) public class ComputerControlSessionProcessorTest { @Mock private ComputerControlSessionProcessor.VirtualDeviceFactory mVirtualDeviceFactory; @Mock private IVirtualDevice mVirtualDevice; private final ComputerControlSessionParams mParams = new ComputerControlSessionParams(); private final Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); private ComputerControlSessionProcessor mProcessor; private AutoCloseable mMockitoSession; @Before public void setUp() { mMockitoSession = MockitoAnnotations.openMocks(this); mParams.displayDpi = 100; mParams.displayHeightPx = 200; mParams.displayWidthPx = 300; mParams.displaySurface = new Surface(); mParams.isDisplayAlwaysUnlocked = true; mParams.name = ComputerControlSessionTest.class.getSimpleName(); when(mVirtualDeviceFactory.createVirtualDevice(any(), any(), any(), any())) .thenReturn(mVirtualDevice); mProcessor = new ComputerControlSessionProcessor(mContext, mVirtualDeviceFactory); } @After public void tearDown() throws Exception { mMockitoSession.close(); } @Test public void maximumNumberOfSessions_isEnforced() throws Exception { ArrayList<IComputerControlSession> sessions = new ArrayList<>(); try { for (int i = 0; i < ComputerControlSessionProcessor.MAXIMUM_CONCURRENT_SESSIONS; ++i) { sessions.add(mProcessor.processNewSession( new Binder(), AttributionSource.myAttributionSource(), mParams)); } assertThrows(UnsupportedOperationException.class, () -> mProcessor.processNewSession( new Binder(), AttributionSource.myAttributionSource(), mParams)); sessions.remove(0).close(); sessions.add(mProcessor.processNewSession( new Binder(), AttributionSource.myAttributionSource(), mParams)); } finally { for (IComputerControlSession session : sessions) { session.close(); } } } } services/tests/servicestests/src/com/android/server/companion/virtual/computercontrol/ComputerControlSessionTest.java +5 −1 Original line number Diff line number Diff line Loading @@ -62,6 +62,8 @@ public class ComputerControlSessionTest { @Mock private ComputerControlSessionProcessor.VirtualDeviceFactory mVirtualDeviceFactory; @Mock private ComputerControlSessionImpl.OnClosedListener mOnClosedListener; @Mock private IVirtualDevice mVirtualDevice; @Captor private ArgumentCaptor<VirtualDeviceParams> mVirtualDeviceParamsArgumentCaptor; Loading Loading @@ -98,7 +100,8 @@ public class ComputerControlSessionTest { .thenReturn(mVirtualDevice); when(mVirtualDevice.createVirtualDisplay(any(), any())).thenReturn(VIRTUAL_DISPLAY_ID); mSession = new ComputerControlSessionImpl(mAppToken, mParams, AttributionSource.myAttributionSource(), mPackageManager, mVirtualDeviceFactory); AttributionSource.myAttributionSource(), mPackageManager, mVirtualDeviceFactory, mOnClosedListener); } @After Loading Loading @@ -160,6 +163,7 @@ public class ComputerControlSessionTest { public void closeSession_closesVirtualDevice() throws Exception { mSession.close(); verify(mVirtualDevice).close(); verify(mOnClosedListener).onClosed(mSession.asBinder()); } @Test Loading Loading
services/companion/java/com/android/server/companion/virtual/computercontrol/ComputerControlSessionImpl.java +23 −2 Original line number Diff line number Diff line Loading @@ -57,10 +57,12 @@ import java.util.concurrent.atomic.AtomicInteger; * A computer control session that encapsulates a {@link IVirtualDevice}. The device is created and * managed by the system, but it is still owned by the caller. */ final class ComputerControlSessionImpl extends IComputerControlSession.Stub { final class ComputerControlSessionImpl extends IComputerControlSession.Stub implements IBinder.DeathRecipient { private final IBinder mAppToken; private final ComputerControlSessionParams mParams; private final OnClosedListener mOnClosedListener; private final IVirtualDevice mVirtualDevice; private final int mVirtualDisplayId; private final IVirtualInputDevice mVirtualTouchscreen; Loading @@ -70,9 +72,11 @@ final class ComputerControlSessionImpl extends IComputerControlSession.Stub { ComputerControlSessionImpl(IBinder appToken, ComputerControlSessionParams params, AttributionSource attributionSource, PackageManager packageManager, ComputerControlSessionProcessor.VirtualDeviceFactory virtualDeviceFactory) { ComputerControlSessionProcessor.VirtualDeviceFactory virtualDeviceFactory, OnClosedListener onClosedListener) { mAppToken = appToken; mParams = params; mOnClosedListener = onClosedListener; VirtualDeviceParams virtualDeviceParams = new VirtualDeviceParams.Builder() .setName(mParams.name) .setDevicePolicy(VirtualDeviceParams.POLICY_TYPE_RECENTS, Loading Loading @@ -147,6 +151,7 @@ final class ComputerControlSessionImpl extends IComputerControlSession.Stub { mVirtualTouchscreen = mVirtualDevice.createVirtualTouchscreen( virtualTouchscreenConfig, new Binder(touchscreenName)); mAppToken.linkToDeath(this, 0); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } Loading Loading @@ -196,6 +201,17 @@ final class ComputerControlSessionImpl extends IComputerControlSession.Stub { @Override public void close() throws RemoteException { mVirtualDevice.close(); mAppToken.unlinkToDeath(this, 0); mOnClosedListener.onClosed(asBinder()); } @Override public void binderDied() { try { close(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } private static class ComputerControlActivityListener Loading @@ -218,4 +234,9 @@ final class ComputerControlSessionImpl extends IComputerControlSession.Stub { @Override public void onSecureWindowHidden(int displayId) {} } /** Interface for listening for closing of sessions. */ interface OnClosedListener { void onClosed(IBinder token); } }
services/companion/java/com/android/server/companion/virtual/computercontrol/ComputerControlSessionProcessor.java +29 −3 Original line number Diff line number Diff line Loading @@ -26,11 +26,19 @@ import android.content.AttributionSource; import android.content.Context; import android.content.pm.PackageManager; import android.os.IBinder; import android.util.ArraySet; import com.android.internal.annotations.VisibleForTesting; public class ComputerControlSessionProcessor { // TODO(b/419548594): Make this configurable. @VisibleForTesting static final int MAXIMUM_CONCURRENT_SESSIONS = 5; private final PackageManager mPackageManager; private final VirtualDeviceFactory mVirtualDeviceFactory; private final ArraySet<IBinder> mSessions = new ArraySet<>(); public ComputerControlSessionProcessor( Context context, VirtualDeviceFactory virtualDeviceFactory) { Loading @@ -46,9 +54,27 @@ public class ComputerControlSessionProcessor { @NonNull AttributionSource attributionSource, @NonNull ComputerControlSessionParams params) { // TODO(b/430259551, b/432678191): Async creation of sessions triggering a consent dialog // TODO(b/419548594): Limit the number of active sessions return new ComputerControlSessionImpl( token, params, attributionSource, mPackageManager, mVirtualDeviceFactory); synchronized (mSessions) { if (mSessions.size() >= MAXIMUM_CONCURRENT_SESSIONS) { // TODO(b/419548594): Communicate this via a callback in an async flow. Returning // null is not good enough and the developer did nothing wrong, so we shouldn't // throw. throw new UnsupportedOperationException( "Maximum number of concurrent session reached, try again later."); } IComputerControlSession session = new ComputerControlSessionImpl( token, params, attributionSource, mPackageManager, mVirtualDeviceFactory, this::onSessionClosed); mSessions.add(session.asBinder()); return session; } } private void onSessionClosed(IBinder token) { synchronized (mSessions) { mSessions.remove(token); } } /** Loading
services/tests/servicestests/src/com/android/server/companion/virtual/computercontrol/ComputerControlSessionProcessorTest.java 0 → 100644 +103 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.server.companion.virtual.computercontrol; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.testng.Assert.assertThrows; import android.companion.virtual.IVirtualDevice; import android.companion.virtual.computercontrol.ComputerControlSessionParams; import android.companion.virtual.computercontrol.IComputerControlSession; import android.content.AttributionSource; import android.content.Context; import android.os.Binder; import android.view.Surface; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; @RunWith(AndroidJUnit4.class) public class ComputerControlSessionProcessorTest { @Mock private ComputerControlSessionProcessor.VirtualDeviceFactory mVirtualDeviceFactory; @Mock private IVirtualDevice mVirtualDevice; private final ComputerControlSessionParams mParams = new ComputerControlSessionParams(); private final Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); private ComputerControlSessionProcessor mProcessor; private AutoCloseable mMockitoSession; @Before public void setUp() { mMockitoSession = MockitoAnnotations.openMocks(this); mParams.displayDpi = 100; mParams.displayHeightPx = 200; mParams.displayWidthPx = 300; mParams.displaySurface = new Surface(); mParams.isDisplayAlwaysUnlocked = true; mParams.name = ComputerControlSessionTest.class.getSimpleName(); when(mVirtualDeviceFactory.createVirtualDevice(any(), any(), any(), any())) .thenReturn(mVirtualDevice); mProcessor = new ComputerControlSessionProcessor(mContext, mVirtualDeviceFactory); } @After public void tearDown() throws Exception { mMockitoSession.close(); } @Test public void maximumNumberOfSessions_isEnforced() throws Exception { ArrayList<IComputerControlSession> sessions = new ArrayList<>(); try { for (int i = 0; i < ComputerControlSessionProcessor.MAXIMUM_CONCURRENT_SESSIONS; ++i) { sessions.add(mProcessor.processNewSession( new Binder(), AttributionSource.myAttributionSource(), mParams)); } assertThrows(UnsupportedOperationException.class, () -> mProcessor.processNewSession( new Binder(), AttributionSource.myAttributionSource(), mParams)); sessions.remove(0).close(); sessions.add(mProcessor.processNewSession( new Binder(), AttributionSource.myAttributionSource(), mParams)); } finally { for (IComputerControlSession session : sessions) { session.close(); } } } }
services/tests/servicestests/src/com/android/server/companion/virtual/computercontrol/ComputerControlSessionTest.java +5 −1 Original line number Diff line number Diff line Loading @@ -62,6 +62,8 @@ public class ComputerControlSessionTest { @Mock private ComputerControlSessionProcessor.VirtualDeviceFactory mVirtualDeviceFactory; @Mock private ComputerControlSessionImpl.OnClosedListener mOnClosedListener; @Mock private IVirtualDevice mVirtualDevice; @Captor private ArgumentCaptor<VirtualDeviceParams> mVirtualDeviceParamsArgumentCaptor; Loading Loading @@ -98,7 +100,8 @@ public class ComputerControlSessionTest { .thenReturn(mVirtualDevice); when(mVirtualDevice.createVirtualDisplay(any(), any())).thenReturn(VIRTUAL_DISPLAY_ID); mSession = new ComputerControlSessionImpl(mAppToken, mParams, AttributionSource.myAttributionSource(), mPackageManager, mVirtualDeviceFactory); AttributionSource.myAttributionSource(), mPackageManager, mVirtualDeviceFactory, mOnClosedListener); } @After Loading Loading @@ -160,6 +163,7 @@ public class ComputerControlSessionTest { public void closeSession_closesVirtualDevice() throws Exception { mSession.close(); verify(mVirtualDevice).close(); verify(mOnClosedListener).onClosed(mSession.asBinder()); } @Test Loading