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

Commit 7b0323c5 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Camera Compat: Track cameraId, and the app and task that had it opened." into main

parents fca1d5b8 8e44b47e
Loading
Loading
Loading
Loading
+283 −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.wm;

import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.os.Process.INVALID_PID;

import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_STATES;

import android.annotation.NonNull;
import android.annotation.Nullable;

import com.android.internal.protolog.ProtoLog;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/** {@link AppCompatCameraStateStrategy} that tracks task-cameraId (and app) pairs. */
class AppCompatCameraStateStrategyForTask implements AppCompatCameraStateStrategy {
    // Data set for app data and active camera IDs since we need to 1) get a camera id by a task
    // when setting up camera compat mode; 2) get a task by a camera id when camera connection is
    // closed and we need to clean up our records.
    private final CameraIdAppPairsSet mCameraAppInfoSet = new CameraIdAppPairsSet();

    // Repository for the newest camera state update. Camera opened and closed signals are processed
    // with a delay. In case of different signals/states pending without being processed, only the
    // newest state will be processed, and the old overwritten.
    private final PendingCameraUpdateRepository mPendingCameraUpdateRepository =
            new PendingCameraUpdateRepository();

    @NonNull
    private final DisplayContent mDisplayContent;

    AppCompatCameraStateStrategyForTask(@NonNull DisplayContent displayContent) {
        mDisplayContent = displayContent;
    }

    @Override
    @NonNull
    public CameraAppInfo trackOnCameraOpened(@NonNull String cameraId,
            @NonNull String packageName) {
        final CameraAppInfo cameraAppInfo = createCameraAppInfo(cameraId, packageName);
        ProtoLog.v(WM_DEBUG_STATES,
                "Display id=%d is notified that Camera %s is open for package %s",
                mDisplayContent.mDisplayId, cameraId, packageName);
        mPendingCameraUpdateRepository.trackPendingCameraOpen(cameraAppInfo);
        return cameraAppInfo;
    }

    @Override
    public void notifyPolicyCameraOpenedIfNeeded(@NonNull CameraAppInfo cameraAppInfo,
            @NonNull AppCompatCameraStatePolicy policy) {
        if (!mPendingCameraUpdateRepository.removePendingCameraOpen(cameraAppInfo)) {
            // Camera compat mode update has happened already or was cancelled
            // because camera was closed.
            return;
        }

        final WindowProcessController cameraApp = getAppProcessForCallingId(cameraAppInfo.mPid);

        if (cameraApp == null) {
            return;
        }
        final ActivityRecord cameraActivity = findUniqueActivityWithPackageName(
                cameraApp.mInfo.packageName);

        if (cameraActivity == null || cameraActivity.getTask() == null) {
            return;
        }

        // TODO(b/423883666): Use `WM_DEBUG_CAMERA_COMPAT`.
        ProtoLog.v(WM_DEBUG_STATES, "CameraOpen: cameraApp=" + cameraApp
                + " cameraInfo.mPid=" + cameraAppInfo.mPid
                + " cameraTask=" + cameraActivity.getTask()
                + " cameraAppInfo.mTaskId=" + cameraAppInfo.mTaskId);

        final boolean anyCameraAlreadyOpenForTask = mCameraAppInfoSet
                .containsAnyCameraForTaskId(cameraActivity.getTask().mTaskId);

        mCameraAppInfoSet.add(cameraAppInfo);
        if (!anyCameraAlreadyOpenForTask) {
            // Only notify listeners if the app has newly opened camera.
            // This does not currently support multiple camera tasks in a single app - this
            // would be a very rare use case (especially for targeted fixed-orientation apps).
            // Given that the camera framework notifies CameraStateMonitor with a packageName
            // and not a task or activity, it would be difficult to correctly and consistently
            // know which task has camera access.
            //
            // The above check is for whether the same task has opened camera, which usually
            // means either the camera was restarted due to config change, or because the app
            // switched between front and back cameras - either way this is not interesting for
            // camera policies.
            // Note: if any camera policy ever needs to dynamically change the treatment based
            // on the camera (front, back, external) this should notify when camera changes and
            // add a method policies can call to check if camera has been running (mostly used
            // to return early).
            policy.onCameraOpened(cameraApp, cameraActivity.getTask());
        }
    }

    @Override
    @NonNull
    public CameraAppInfo trackOnCameraClosed(@NonNull String cameraId) {
        // This function is synchronous, and cameraClosed signal will come before cameraOpened.
        // Therefore, there will be only one app recorded with this camera opened.
        CameraAppInfo cameraAppInfo = mCameraAppInfoSet.getAnyCameraAppStateForCameraId(cameraId);
        if (cameraAppInfo == null) {
            ProtoLog.w(WM_DEBUG_STATES, "Camera closed but cannot find the app which had it"
                    + " opened.");
            cameraAppInfo = new CameraAppInfo(cameraId, INVALID_PID, INVALID_TASK_ID, null);
        }
        mPendingCameraUpdateRepository.trackPendingCameraClose(cameraAppInfo);
        return cameraAppInfo;
    }

    @Override
    public boolean notifyPolicyCameraClosedIfNeeded(@NonNull CameraAppInfo cameraAppInfo,
            @NonNull AppCompatCameraStatePolicy policy) {
        if (!mPendingCameraUpdateRepository.removePendingCameraClose(cameraAppInfo)) {
            // Already reconnected to this camera, no need to clean up.
            return true;
        }

        final Task cameraTask = mDisplayContent.getTask(task ->
                task.getTaskInfo().taskId == cameraAppInfo.mTaskId);
        final boolean canClose = cameraTask == null
                || policy.canCameraBeClosed(cameraAppInfo.mCameraId, cameraTask);
        if (canClose) {
            // Finish cleaning up. Remove only cameraId of this particular task.
            mCameraAppInfoSet.remove(cameraAppInfo);
            if (!mCameraAppInfoSet.containsAnyCameraForTaskId(cameraAppInfo.mTaskId)) {
                final WindowProcessController app = getAppProcessForCallingId(
                        cameraAppInfo.mPid);
                // Only notify the listeners if the camera is not running - this close signal
                // could be from switching cameras (e.g. back to front camera, and vice versa).
                policy.onCameraClosed(app, cameraTask);
            }
        }

        return canClose;
    }

    @Override
    public boolean isCameraRunningForActivity(@NonNull ActivityRecord activity) {
        return activity.getTask() != null && mCameraAppInfoSet
                .containsAnyCameraForTaskId(activity.getTask().mTaskId);
    }

    // TODO(b/336474959): try to decouple `cameraId` from the listeners.
    @Override
    public boolean isCameraWithIdRunningForActivity(@NonNull ActivityRecord activity,
            @NonNull String cameraId) {
        return activity.getTask() != null && mCameraAppInfoSet
                .containsCameraIdAndTask(cameraId, activity.getTask().mTaskId);
    }

    @NonNull
    private CameraAppInfo createCameraAppInfo(@NonNull String cameraId,
            @Nullable String packageName) {
        final ActivityRecord cameraActivity = packageName == null ? null
                : findUniqueActivityWithPackageName(packageName);
        final Task cameraTask = cameraActivity == null ? null : cameraActivity.getTask();
        final WindowProcessController cameraApp = cameraActivity == null ? null
                : cameraActivity.app;
        return new CameraAppInfo(cameraId,
                cameraApp == null ? INVALID_PID : cameraApp.getPid(),
                cameraTask == null ? INVALID_TASK_ID : cameraTask.mTaskId,
                packageName);
    }

    // TODO(b/335165310): verify that this works in multi instance and permission dialogs.
    /**
     * Finds a visible activity with the given package name.
     *
     * <p>If there are multiple visible activities with a given package name, and none of them are
     * the `topRunningActivity`, returns null.
     */
    @Nullable
    private ActivityRecord findUniqueActivityWithPackageName(@NonNull String packageName) {
        final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
                /* considerKeyguardState= */ true);
        if (topActivity != null && topActivity.packageName.equals(packageName)) {
            return topActivity;
        }

        final List<ActivityRecord> activitiesOfPackageWhichOpenedCamera = new ArrayList<>();
        mDisplayContent.forAllActivities(activityRecord -> {
            if (activityRecord.isVisibleRequested()
                    && activityRecord.packageName.equals(packageName)) {
                activitiesOfPackageWhichOpenedCamera.add(activityRecord);
            }
        });

        if (activitiesOfPackageWhichOpenedCamera.isEmpty()) {
            ProtoLog.w(WM_DEBUG_STATES, "Cannot find camera activity.");
            return null;
        }

        if (activitiesOfPackageWhichOpenedCamera.size() == 1) {
            return activitiesOfPackageWhichOpenedCamera.getFirst();
        }

        // Return null if we cannot determine which activity opened camera. This is preferred to
        // applying treatment to the wrong activity.
        ProtoLog.w(WM_DEBUG_STATES, "Cannot determine which activity opened camera.");
        return null;
    }

    @NonNull
    public String toString() {
        return " mCameraAppInfoSet=" + mCameraAppInfoSet
                .getSummaryForDisplayRotationHistoryRecord();
    }

    @Nullable
    private WindowProcessController getAppProcessForCallingId(int pid) {
        return mDisplayContent.mAtmService.mProcessMap.getProcess(pid);
    }

    /**
     * Repository for @{@link CameraAppInfo}s and the camera status changes (opening or closing
     * camera) that are in-flight.
     *
     * <p>As camera opening and closing have different delays, and some of them are quick switches
     * caused by activity refresh or front/back camera switch, tracking pending states enables
     * to skip brief activity changes which cause flickering.
     */
    static class PendingCameraUpdateRepository {
        /** Enum describing the newest camera state that is not yet processed. */
        enum PendingCameraState {
            OPENED,
            CLOSED
        }

        /**
         * Set of apps that have camera status update (newly opened or closed) scheduled to be
         * processed.
         *
         * <p>Existing state will be overwritten, as the newest signal (opened/closed) should be
         * respected.
         */
        private final HashMap<CameraAppInfo, PendingCameraState> mPendingCameraStateMap =
                new HashMap<>();

        void trackPendingCameraOpen(@NonNull CameraAppInfo cameraAppInfo) {
            // Some apps can’t handle configuration changes coming at the same time with Camera
            // setup so delaying orientation update to accommodate for that.
            mPendingCameraStateMap.put(cameraAppInfo, PendingCameraState.OPENED);
        }

        void trackPendingCameraClose(@NonNull CameraAppInfo cameraAppInfo) {
            mPendingCameraStateMap.put(cameraAppInfo, PendingCameraState.CLOSED);
        }

        /**
         * @return true if camera open was pending for given {@param cameraAppInfo}.
         */
        boolean removePendingCameraOpen(@NonNull CameraAppInfo cameraAppInfo) {
            return mPendingCameraStateMap.remove(cameraAppInfo, PendingCameraState.OPENED);
        }

        /**
         * @return true if camera close was pending for given {@param cameraAppInfo}.
         */
        boolean removePendingCameraClose(@NonNull CameraAppInfo cameraAppInfo) {
            return mPendingCameraStateMap.remove(cameraAppInfo, PendingCameraState.CLOSED);
        }
    }
}
+83 −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.wm;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.ArraySet;

/**
 * Data set for the currently active cameraId and the app that opened it.
 *
 * <p>This class is not thread-safe.
 */
final class CameraIdAppPairsSet {
    private final ArraySet<CameraAppInfo> mCameraAppInfoSet = new ArraySet<>();

    boolean isEmpty() {
        return mCameraAppInfoSet.isEmpty();
    }

    void add(@NonNull CameraAppInfo cameraAppInfo) {
        mCameraAppInfoSet.add(cameraAppInfo);
    }

    boolean containsAnyCameraForTaskId(int taskId) {
        for (int i = 0; i < mCameraAppInfoSet.size(); i++) {
            final CameraAppInfo info = mCameraAppInfoSet.valueAt(i);
            if (info.mTaskId == taskId) {
                return true;
            }
        }
        return false;
    }

    @Nullable
    CameraAppInfo getAnyCameraAppStateForCameraId(@NonNull String cameraId) {
        for (int i = 0; i < mCameraAppInfoSet.size(); i++) {
            final CameraAppInfo info = mCameraAppInfoSet.valueAt(i);
            if (info.mCameraId.equals(cameraId)) {
                return info;
            }
        }
        return null;
    }

    boolean containsCameraIdAndTask(@NonNull String cameraId, int taskId) {
        for (int i = 0; i < mCameraAppInfoSet.size(); i++) {
            final CameraAppInfo info = mCameraAppInfoSet.valueAt(i);
            if (info.mCameraId.equals(cameraId) && info.mTaskId == taskId) {
                return true;
            }
        }
        return false;
    }

    boolean remove(@NonNull CameraAppInfo cameraAppInfo) {
        return mCameraAppInfoSet.remove(cameraAppInfo);
    }

    @NonNull
    String getSummaryForDisplayRotationHistoryRecord() {
        return "{ mCameraAppInfoSet=" + mCameraAppInfoSet + " }";
    }

    @Override
    public String toString() {
        return getSummaryForDisplayRotationHistoryRecord();
    }
}
+4 −1
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import android.os.Handler;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;
import com.android.window.flags.Flags;

/**
 * Class that listens to camera open/closed signals, keeps track of the current apps using camera,
@@ -92,7 +93,9 @@ class CameraStateMonitor {
        mAppCompatCameraStatePolicy = appCompatCameraStatePolicy;
        mWmService = displayContent.mWmService;
        mCameraManager = mWmService.mContext.getSystemService(CameraManager.class);
        mAppCompatCameraStateStrategy = new AppCompatCameraStateStrategyForPackage(displayContent);
        mAppCompatCameraStateStrategy = Flags.enableCameraCompatTrackTaskAndAppBugfix()
                ? new AppCompatCameraStateStrategyForTask(displayContent)
                : new AppCompatCameraStateStrategyForPackage(displayContent);
    }

    /** Starts listening to camera opened/closed signals. */
+325 −0

File added.

Preview size limit exceeded, changes collapsed.

+152 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.wm;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import android.platform.test.annotations.Presubmit;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Tests for {@link CameraIdAppPairsSet}.
 *
 * Build/Install/Run:
 * atest WmTests:CameraIdAppPairsSetTests
 */
@Presubmit
@RunWith(WindowTestRunner.class)
public class CameraIdAppPairsSetTests {
    private CameraIdAppPairsSet mMapping;

    private static final String TEST_PACKAGE_1 = "com.test.package.one";
    private static final String TEST_PACKAGE_2 = "com.test.package.two";
    private static final int TASK_ID_1 = 1;
    private static final int TASK_ID_2 = 2;
    private static final int PID_1 = 101;
    private static final int PID_2 = 102;
    private static final String CAMERA_ID_1 = "1234";
    private static final String CAMERA_ID_2 = "5678";
    private final CameraAppInfo mCameraAppInfo1Camera1 =
            new CameraAppInfo(CAMERA_ID_1, PID_1, TASK_ID_1, TEST_PACKAGE_1);
    private final CameraAppInfo mCameraAppInfo1Camera2 =
            new CameraAppInfo(CAMERA_ID_2, PID_1, TASK_ID_1, TEST_PACKAGE_1);
    private final CameraAppInfo mCameraAppInfo2Camera1 =
            new CameraAppInfo(CAMERA_ID_1, PID_2, TASK_ID_2, TEST_PACKAGE_2);
    private final CameraAppInfo mCameraAppInfo2Camera2 =
            new CameraAppInfo(CAMERA_ID_2, PID_2, TASK_ID_2, TEST_PACKAGE_2);

    @Before
    public void setUp() {
        mMapping = new CameraIdAppPairsSet();
    }

    @Test
    public void mappingEmptyAtStart() {
        assertTrue(mMapping.isEmpty());
    }

    @Test
    public void addTaskAndCameraId_containsCameraIdAndTask() {
        mMapping.add(mCameraAppInfo1Camera1);

        assertTrue(mMapping.containsCameraIdAndTask(CAMERA_ID_1, mCameraAppInfo1Camera1.mTaskId));
    }

    @Test
    public void addTwoTasksAndCameraIds_containsCameraIdsAndTasks() {
        mMapping.add(mCameraAppInfo1Camera1);
        mMapping.add(mCameraAppInfo2Camera2);

        assertTrue(mMapping.containsCameraIdAndTask(CAMERA_ID_1, mCameraAppInfo1Camera1.mTaskId));
        assertTrue(mMapping.containsCameraIdAndTask(CAMERA_ID_2, mCameraAppInfo2Camera2.mTaskId));
    }

    @Test
    public void addTwoTasksAndCameraIds_checkContainsCameraIdAndTaskFromDifferentPair_false() {
        mMapping.add(mCameraAppInfo1Camera1);
        mMapping.add(mCameraAppInfo2Camera2);

        assertFalse(mMapping.containsCameraIdAndTask(CAMERA_ID_1, mCameraAppInfo2Camera2.mTaskId));
    }

    @Test
    public void addTwoTasksForTheSameCamera_bothExist() {
        mMapping.add(mCameraAppInfo1Camera1);
        mMapping.add(mCameraAppInfo2Camera2);

        assertTrue(mMapping.containsCameraIdAndTask(CAMERA_ID_1, mCameraAppInfo1Camera1.mTaskId));
        assertTrue(mMapping.containsCameraIdAndTask(CAMERA_ID_2, mCameraAppInfo2Camera2.mTaskId));
    }

    @Test
    public void addTwoCamerasForTheSameTask_bothExist() {
        mMapping.add(mCameraAppInfo1Camera1);
        mMapping.add(mCameraAppInfo1Camera2);

        assertTrue(mMapping.containsCameraIdAndTask(CAMERA_ID_1, mCameraAppInfo1Camera1.mTaskId));
        assertTrue(mMapping.containsCameraIdAndTask(CAMERA_ID_2, mCameraAppInfo1Camera2.mTaskId));
    }

    @Test
    public void addTwoTasksForTheSameCamera_returnsAnyTask() {
        mMapping.add(mCameraAppInfo1Camera1);
        mMapping.add(mCameraAppInfo2Camera1);

        assertNotNull(mMapping.getAnyCameraAppStateForCameraId(CAMERA_ID_1));
    }

    @Test
    public void addTwoCamerasForTheSameTask_containsAnyCamera() {
        mMapping.add(mCameraAppInfo1Camera1);
        mMapping.add(mCameraAppInfo1Camera2);

        assertTrue(mMapping.containsAnyCameraForTaskId(mCameraAppInfo1Camera1.mTaskId));
    }

    @Test
    public void addAndRemoveCameraId_containsOtherCameraAndTask() {
        mMapping.add(mCameraAppInfo1Camera1);
        mMapping.add(mCameraAppInfo2Camera2);

        mMapping.remove(mCameraAppInfo1Camera1);

        assertTrue(mMapping.containsCameraIdAndTask(CAMERA_ID_2, mCameraAppInfo2Camera2.mTaskId));
    }

    @Test
    public void addAndRemoveOnlyCameraId_empty() {
        mMapping.add(mCameraAppInfo1Camera1);
        mMapping.remove(mCameraAppInfo1Camera1);

        assertTrue(mMapping.isEmpty());
    }

    @Test
    public void addAndRemoveOnlyCameraIdUsingEqualObject_empty() {
        mMapping.add(mCameraAppInfo1Camera1);
        mMapping.remove(new CameraAppInfo(mCameraAppInfo1Camera1.mCameraId,
                mCameraAppInfo1Camera1.mPid, mCameraAppInfo1Camera1.mTaskId,
                mCameraAppInfo1Camera1.mPackageName));

        assertTrue(mMapping.isEmpty());
    }
}