Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 199f05cf authored by [D's avatar [D[1;5D
Browse files

Primitive enforcement of session limit

Bug: 419548594
Test: atest
Flag: android.companion.virtualdevice.flags.computer_control_access
Change-Id: Ie46ff31c7afd5e31c0b46ee73f2fca8464610f59
parent 0a699200
Loading
Loading
Loading
Loading
+23 −2
Original line number Diff line number Diff line
@@ -56,10 +56,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;
@@ -69,9 +71,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,
@@ -146,6 +150,7 @@ final class ComputerControlSessionImpl extends IComputerControlSession.Stub {
            mVirtualTouchscreen = mVirtualDevice.createVirtualTouchscreen(
                    virtualTouchscreenConfig, new Binder(touchscreenName));

            mAppToken.linkToDeath(this, 0);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
@@ -191,6 +196,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
@@ -213,4 +229,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);
    }
}
+29 −3
Original line number Diff line number Diff line
@@ -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) {
@@ -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);
        }
    }

    /**
+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();
            }
        }
    }
}
+5 −1
Original line number Diff line number Diff line
@@ -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;
@@ -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
@@ -160,6 +163,7 @@ public class ComputerControlSessionTest {
    public void closeSession_closesVirtualDevice() throws Exception {
        mSession.close();
        verify(mVirtualDevice).close();
        verify(mOnClosedListener).onClosed(mSession.asBinder());
    }

    @Test