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

Commit 1bf2e3cb authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Block camera access for apps running on VirtualDevices"

parents c7224ea6 d1528caf
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