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

Commit 40ae3f5a authored by Vladimir Komsiyski's avatar Vladimir Komsiyski Committed by Android (Google) Code Review
Browse files

Merge "Primitive enforcement of session limit" into main

parents f55f3477 199f05cf
Loading
Loading
Loading
Loading
+23 −2
Original line number Diff line number Diff line
@@ -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;
@@ -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,
@@ -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();
        }
@@ -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
@@ -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);
    }
}
+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