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

Commit d1528caf authored by Antony Sargent's avatar Antony Sargent
Browse files

Block camera access for apps running on VirtualDevices

Bug: 170935709
Test: atest CameraAccessControllerTest
Change-Id: I15beeb0d4aa5aa3588e70ef37542192997121499
parent c0715a6c
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -6266,4 +6266,8 @@ ul.</string>
    </string>
    <!-- Action label of notification for user to check background apps. [CHAR LIMIT=NONE]  -->
    <string name="notification_action_check_bg_apps">Check active apps</string>

    <!-- Strings for VirtualDeviceManager -->
    <!-- Error message indicating the camera cannot be accessed when running on a virtual device. [CHAR LIMIT=NONE] -->
    <string name="vdm_camera_access_denied">Cannot access camera from this device</string>
</resources>
+3 −0
Original line number Diff line number Diff line
@@ -4724,5 +4724,8 @@
  <java-symbol type="bool" name="config_lowPowerStandbyEnabledByDefault" />
  <java-symbol type="integer" name="config_lowPowerStandbyNonInteractiveTimeout" />

  <!-- For VirtualDeviceManager -->
  <java-symbol type="string" name="vdm_camera_access_denied" />

  <java-symbol type="color" name="camera_privacy_light"/>
</resources>
+212 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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;

import static android.hardware.camera2.CameraInjectionSession.InjectionStatusCallback.ERROR_INJECTION_UNSUPPORTED;

import android.annotation.NonNull;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraInjectionSession;
import android.hardware.camera2.CameraManager;
import android.util.ArrayMap;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;

/**
 * Handles blocking access to the camera for apps running on virtual devices.
 */
class CameraAccessController extends CameraManager.AvailabilityCallback {
    private static final String TAG = "CameraAccessController";

    private final Object mLock = new Object();

    private final Context mContext;
    private VirtualDeviceManagerInternal mVirtualDeviceManagerInternal;
    CameraAccessBlockedCallback mBlockedCallback;
    private CameraManager mCameraManager;
    private boolean mListeningForCameraEvents;
    private PackageManager mPackageManager;

    @GuardedBy("mLock")
    private ArrayMap<String, InjectionSessionData> mPackageToSessionData = new ArrayMap<>();

    static class InjectionSessionData {
        public int appUid;
        public ArrayMap<String, CameraInjectionSession> cameraIdToSession = new ArrayMap<>();
    }

    interface CameraAccessBlockedCallback {
        /**
         * Called whenever an app was blocked from accessing a camera.
         * @param appUid uid for the app which was blocked
         */
        void onCameraAccessBlocked(int appUid);
    }

    CameraAccessController(Context context,
            VirtualDeviceManagerInternal virtualDeviceManagerInternal,
            CameraAccessBlockedCallback blockedCallback) {
        mContext = context;
        mVirtualDeviceManagerInternal = virtualDeviceManagerInternal;
        mBlockedCallback = blockedCallback;
        mCameraManager = mContext.getSystemService(CameraManager.class);
        mPackageManager = mContext.getPackageManager();
    }

    /**
     * Starts watching for camera access by uids running on a virtual device, if we were not
     * already doing so.
     */
    public void startObservingIfNeeded() {
        synchronized (mLock) {
            if (!mListeningForCameraEvents) {
                mCameraManager.registerAvailabilityCallback(mContext.getMainExecutor(), this);
                mListeningForCameraEvents = true;
            }
        }
    }

    /**
     * Stop watching for camera access.
     */
    public void stopObserving() {
        synchronized (mLock) {
            mCameraManager.unregisterAvailabilityCallback(this);
            mListeningForCameraEvents = false;
        }
    }

    @Override
    public void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) {
        synchronized (mLock) {
            try {
                final ApplicationInfo ainfo =
                        mPackageManager.getApplicationInfo(packageName, 0);
                InjectionSessionData data = mPackageToSessionData.get(packageName);
                if (!mVirtualDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(ainfo.uid)) {
                    CameraInjectionSession existingSession =
                            (data != null) ? data.cameraIdToSession.get(cameraId) : null;
                    if (existingSession != null) {
                        existingSession.close();
                        data.cameraIdToSession.remove(cameraId);
                        if (data.cameraIdToSession.isEmpty()) {
                            mPackageToSessionData.remove(packageName);
                        }
                    }
                    return;
                }
                if (data == null) {
                    data = new InjectionSessionData();
                    data.appUid = ainfo.uid;
                    mPackageToSessionData.put(packageName, data);
                }
                if (data.cameraIdToSession.containsKey(cameraId)) {
                    return;
                }
                startBlocking(packageName, cameraId);
            } catch (PackageManager.NameNotFoundException e) {
                Slog.e(TAG, "onCameraOpened - unknown package " + packageName, e);
                return;
            }
        }
    }

    @Override
    public void onCameraClosed(@NonNull String cameraId) {
        synchronized (mLock) {
            for (int i = mPackageToSessionData.size() - 1; i >= 0; i--) {
                InjectionSessionData data = mPackageToSessionData.valueAt(i);
                CameraInjectionSession session = data.cameraIdToSession.get(cameraId);
                if (session != null) {
                    session.close();
                    data.cameraIdToSession.remove(cameraId);
                    if (data.cameraIdToSession.isEmpty()) {
                        mPackageToSessionData.removeAt(i);
                    }
                }
            }
        }
    }

    /**
     * Turns on blocking for a particular camera and package.
     */
    private void startBlocking(String packageName, String cameraId) {
        try {
            mCameraManager.injectCamera(packageName, cameraId, /* externalCamId */ "",
                    mContext.getMainExecutor(),
                    new CameraInjectionSession.InjectionStatusCallback() {
                        @Override
                        public void onInjectionSucceeded(
                                @NonNull CameraInjectionSession session) {
                            CameraAccessController.this.onInjectionSucceeded(cameraId, packageName,
                                    session);
                        }

                        @Override
                        public void onInjectionError(@NonNull int errorCode) {
                            CameraAccessController.this.onInjectionError(cameraId, packageName,
                                    errorCode);
                        }
                    });
        } catch (CameraAccessException e) {
            Slog.e(TAG,
                    "Failed to injectCamera for cameraId:" + cameraId + " package:" + packageName,
                    e);
        }
    }

    private void onInjectionSucceeded(String cameraId, String packageName,
            @NonNull CameraInjectionSession session) {
        synchronized (mLock) {
            InjectionSessionData data = mPackageToSessionData.get(packageName);
            if (data == null) {
                Slog.e(TAG, "onInjectionSucceeded didn't find expected entry for package "
                        + packageName);
                session.close();
                return;
            }
            CameraInjectionSession existingSession = data.cameraIdToSession.put(cameraId, session);
            if (existingSession != null) {
                Slog.e(TAG, "onInjectionSucceeded found unexpected existing session for camera "
                        + cameraId);
                existingSession.close();
            }
        }
    }

    private void onInjectionError(String cameraId, String packageName, @NonNull int errorCode) {
        if (errorCode != ERROR_INJECTION_UNSUPPORTED) {
            // ERROR_INJECTION_UNSUPPORTED means that there wasn't an external camera to map to the
            // internal camera, which is expected when using the injection interface as we are in
            // this class to simply block camera access. Any other error is unexpected.
            Slog.e(TAG, "Unexpected injection error code:" + errorCode + " for camera:" + cameraId
                    + " and package:" + packageName);
            return;
        }
        synchronized (mLock) {
            InjectionSessionData data = mPackageToSessionData.get(packageName);
            if (data != null) {
                mBlockedCallback.onCameraAccessBlocked(data.appUid);
            }
        }
    }
}
+23 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE

import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.StringRes;
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.PendingIntent;
@@ -57,6 +58,8 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.widget.Toast;
import android.window.DisplayWindowPolicyController;

import com.android.internal.annotations.GuardedBy;
@@ -583,6 +586,26 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
        return false;
    }

    /**
     * Shows a toast on virtual displays owned by this device which have a given uid running.
     */
    void showToastWhereUidIsRunning(int uid, @StringRes int resId, @Toast.Duration int duration) {
        synchronized (mVirtualDeviceLock) {
            DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
            final int size = mWindowPolicyControllers.size();
            for (int i = 0; i < size; i++) {
                if (mWindowPolicyControllers.valueAt(i).containsUid(uid)) {
                    int displayId = mWindowPolicyControllers.keyAt(i);
                    Display display = displayManager.getDisplay(displayId);
                    if (display != null && display.isValid()) {
                        Toast.makeText(mContext.createDisplayContext(display), resId,
                                duration).show();
                    }
                }
            }
        }
    }

    interface OnDeviceCloseListener {
        void onClose(int associationId);
    }
+21 −2
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ import android.os.RemoteException;
import android.util.ExceptionUtils;
import android.util.Slog;
import android.util.SparseArray;
import android.widget.Toast;
import android.window.DisplayWindowPolicyController;

import com.android.internal.annotations.GuardedBy;
@@ -62,8 +63,10 @@ public class VirtualDeviceManagerService extends SystemService {

    private final Object mVirtualDeviceManagerLock = new Object();
    private final VirtualDeviceManagerImpl mImpl;
    private VirtualDeviceManagerInternal mLocalService;
    private final Handler mHandler = new Handler(Looper.getMainLooper());
    private final PendingTrampolineMap mPendingTrampolines = new PendingTrampolineMap(mHandler);
    private final CameraAccessController mCameraAccessController;

    /**
     * Mapping from CDM association IDs to virtual devices. Only one virtual device is allowed for
@@ -90,6 +93,9 @@ public class VirtualDeviceManagerService extends SystemService {
    public VirtualDeviceManagerService(Context context) {
        super(context);
        mImpl = new VirtualDeviceManagerImpl();
        mLocalService = new LocalService();
        mCameraAccessController = new CameraAccessController(getContext(), mLocalService,
                this::onCameraAccessBlocked);
    }

    private final ActivityInterceptorCallback mActivityInterceptorCallback =
@@ -118,8 +124,7 @@ public class VirtualDeviceManagerService extends SystemService {
    @Override
    public void onStart() {
        publishBinderService(Context.VIRTUAL_DEVICE_SERVICE, mImpl);
        publishLocalService(VirtualDeviceManagerInternal.class, new LocalService());

        publishLocalService(VirtualDeviceManagerInternal.class, mLocalService);
        ActivityTaskManagerInternal activityTaskManagerInternal = getLocalService(
                ActivityTaskManagerInternal.class);
        activityTaskManagerInternal.registerActivityStartInterceptor(
@@ -169,6 +174,16 @@ public class VirtualDeviceManagerService extends SystemService {
        }
    }

    void onCameraAccessBlocked(int appUid) {
        synchronized (mVirtualDeviceManagerLock) {
            int size = mVirtualDevices.size();
            for (int i = 0; i < size; i++) {
                mVirtualDevices.valueAt(i).showToastWhereUidIsRunning(appUid,
                        com.android.internal.R.string.vdm_camera_access_denied, Toast.LENGTH_LONG);
            }
        }
    }

    class VirtualDeviceManagerImpl extends IVirtualDeviceManager.Stub implements
            VirtualDeviceImpl.PendingTrampolineCallback {

@@ -205,10 +220,14 @@ public class VirtualDeviceManagerService extends SystemService {
                            public void onClose(int associationId) {
                                synchronized (mVirtualDeviceManagerLock) {
                                    mVirtualDevices.remove(associationId);
                                    if (mVirtualDevices.size() == 0) {
                                        mCameraAccessController.stopObserving();
                                    }
                                }
                            }
                        },
                        this, activityListener, params);
                mCameraAccessController.startObservingIfNeeded();
                mVirtualDevices.put(associationInfo.getId(), virtualDevice);
                return virtualDevice;
            }
Loading