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

Commit c81d26cd authored by Mina Granic's avatar Mina Granic
Browse files

Extract camera tracking into AppCompatCameraStateStrategy.

This change allows refactoring of the way cameraId and app that opened it are tracked (see child CL).

Flag: com.android.window.flags.enable_camera_compat_track_task_and_app_bugfix
Test: atest WmTests:CameraStateMonitorTests
Test: atest WmTests:AppCompatCameraStateStrategyForPackageTests
Bug: 380840084
Change-Id: I38500357f30c7c48b5d5574c25099a4c3556569e
parent 076518a4
Loading
Loading
Loading
Loading
+80 −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;

/**
 * Interface for tracking camera state, and notifying {@link AppCompatCameraStatePolicy} of changes.
 *
 * <p>{@link AppCompatCameraStateStrategy} implementations should track which apps hold the camera
 * access, and any ongoing camera state changes changes. 'track' methods should always be called
 * before appropriate 'maybeNotify' methods for the same task-cameraId pair, but the order of
 * open/close can vary, for example due to built-in delays from the caller.
 */
interface AppCompatCameraStateStrategy {
    /**
     * Allows saving cameraId, to be processed later on
     * {@link AppCompatCameraStateStrategy#notifyPolicyCameraOpenedIfNeeded} after a delay.
     *
     * <p>The {@link AppCompatCameraStateStrategy} should track which camera operations have been
     * started (delayed), as camera opened/closed operations often compete with each other, and due
     * to built-in delays can cause different order of these operations when they are finally
     * processed. Examples of quickly closing and opening the camera: activity relaunch due to
     * configuration change, switching front/back cameras, new app requesting camera and taking the
     * access rights away from the existing camera app.
     */
    void trackOnCameraOpened(@NonNull String cameraId);

    /**
     * Processes camera opened signal, and if the change is relevant for {@link
     * AppCompatCameraStatePolicy} calls {@link AppCompatCameraStatePolicy#onCameraOpened}.
     */
    void notifyPolicyCameraOpenedIfNeeded(@NonNull String cameraId, @NonNull String packageName,
            @NonNull AppCompatCameraStatePolicy policy);

    /**
     * Allows saving cameraId to be processed later on
     * {@link AppCompatCameraStateStrategy#notifyPolicyCameraClosedIfNeeded} after a delay.
     *
     * <p>The {@link AppCompatCameraStateStrategy} should track which camera operations have been
     * started (delayed), as camera opened/closed operations often compete with each other, and due
     * to built-in delays can cause different order of these operations when they are finally
     * processed. Examples of quickly closing and opening the camera: activity relaunch due to
     * configuration change, switching front/back cameras, new app requesting camera and taking the
     * access rights away from the existing camera app.
     */
    void trackOnCameraClosed(@NonNull String cameraId);


    /**
     * Processes camera closed signal, and if the change is relevant for {@link
     * AppCompatCameraStatePolicy} calls {@link AppCompatCameraStatePolicy#onCameraClosed}.
     *
     * @return true if policies were able to handle the camera closed event, or false if it needs to
     * be rescheduled.
     */
    boolean notifyPolicyCameraClosedIfNeeded(@NonNull String cameraId,
            @NonNull AppCompatCameraStatePolicy policy);

    /** Returns whether a given activity holds any camera opened. */
    boolean isCameraRunningForActivity(@NonNull ActivityRecord activity);

    /** Returns whether a given activity holds a specific camera opened. */
    // TODO(b/336474959): try to decouple `cameraId` from the listeners.
    boolean isCameraWithIdRunningForActivity(@NonNull ActivityRecord activity,
            @NonNull String cameraId);
}
+166 −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 com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;

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

import java.util.ArrayList;
import java.util.Set;

/** {@link AppCompatCameraStateStrategy} that tracks packageName-cameraId pairs. */
class AppCompatCameraStateStrategyForPackage implements AppCompatCameraStateStrategy {
    private static final String TAG = TAG_WITH_CLASS_NAME ? "AppCompatCameraStateStrategyForPackage"
            : TAG_WM;
    @NonNull
    private final DisplayContent mDisplayContent;

    // Bi-directional map between package names and active camera IDs since we need to 1) get a
    // camera id by a package name when resizing the window; 2) get a package name by a camera id
    // when camera connection is closed and we need to clean up our records.
    private final CameraIdPackageNameBiMapping mCameraIdPackageBiMapping =
            new CameraIdPackageNameBiMapping();
    // TODO(b/380840084): Consider making this a set of CameraId/PackageName pairs. This is to
    // keep track of camera-closed signals when apps are switching camera access, so that the policy
    // can restore app configuration when an app closes camera (e.g. loses camera access due to
    // another app).
    private final Set<String> mScheduledToBeRemovedCameraIdSet = new ArraySet<>();

    private final Set<String> mScheduledCompatModeUpdateCameraIdSet = new ArraySet<>();

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

    @Override
    public void trackOnCameraOpened(@NonNull String cameraId) {
        mScheduledToBeRemovedCameraIdSet.remove(cameraId);
        mScheduledCompatModeUpdateCameraIdSet.add(cameraId);
    }

    @Override
    public void notifyPolicyCameraOpenedIfNeeded(@NonNull String cameraId,
            @NonNull String packageName, @NonNull AppCompatCameraStatePolicy policy) {
        if (!mScheduledCompatModeUpdateCameraIdSet.remove(cameraId)) {
            // Camera compat mode update has happened already or was cancelled
            // because camera was closed.
            return;
        }
        mCameraIdPackageBiMapping.put(packageName, cameraId);
        // If there are multiple activities of the same package name and none of
        // them are the top running activity, we do not apply treatment (rather than
        // guessing and applying it to the wrong activity).
        final ActivityRecord cameraActivity = findUniqueActivityWithPackageName(packageName);
        if (cameraActivity == null) {
            // If camera is active, activity, task and app process must exist. No need to notify
            // listeners or track the package otherwise.
            return;
        }
        policy.onCameraOpened(cameraActivity);
    }

    @Override
    public void trackOnCameraClosed(@NonNull String cameraId) {
        mScheduledToBeRemovedCameraIdSet.add(cameraId);
        // No need to update window size for this camera if it's already closed.
        mScheduledCompatModeUpdateCameraIdSet.remove(cameraId);
    }

    @Override
    public boolean notifyPolicyCameraClosedIfNeeded(@NonNull String cameraId,
            @NonNull AppCompatCameraStatePolicy policy) {
        if (!mScheduledToBeRemovedCameraIdSet.remove(cameraId)) {
            // Already reconnected to this camera, no need to clean up.
            return true;
        }
        final boolean canClose = policy.canCameraBeClosed(cameraId);
        if (canClose) {
            // Finish cleaning up.
            mCameraIdPackageBiMapping.removeCameraId(cameraId);
            policy.onCameraClosed();
            return true;
        } else {
            mScheduledToBeRemovedCameraIdSet.add(cameraId);
            // Not ready to process closure yet - the camera activity might be refreshing.
            // Try again later.
            return false;
        }
    }

    @Override
    public boolean isCameraRunningForActivity(@NonNull ActivityRecord activity) {
        return getCameraIdForActivity(activity) != null;
    }

    @Override
    public boolean isCameraWithIdRunningForActivity(@NonNull ActivityRecord activity,
            @NonNull String cameraId) {
        return cameraId.equals(getCameraIdForActivity(activity));
    }

    /** Returns the information about apps using camera, for logging purposes. */
    private String getCameraIdForActivity(@NonNull ActivityRecord activity) {
        return mCameraIdPackageBiMapping.getCameraId(activity.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 ArrayList<ActivityRecord> activitiesOfPackageWhichOpenedCamera = new ArrayList<>();
        mDisplayContent.forAllActivities(activityRecord -> {
            if (activityRecord.isVisibleRequested()
                    && activityRecord.packageName.equals(packageName)) {
                activitiesOfPackageWhichOpenedCamera.add(activityRecord);
            }
        });

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

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

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

    @Override
    public String toString() {
        return "CameraIdPackageNameBiMapping=" + mCameraIdPackageBiMapping.toString();
    }
}
+2 −1
Original line number Diff line number Diff line
@@ -63,7 +63,8 @@ final class CameraIdPackageNameBiMapping {
    }

    @NonNull
    String getSummaryForDisplayRotationHistoryRecord() {
    @Override
    public String toString() {
        return "{ mPackageToCameraIdMap=" + mPackageToCameraIdMap + " }";
    }

+35 −121
Original line number Diff line number Diff line
@@ -23,23 +23,16 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.hardware.camera2.CameraManager;
import android.os.Handler;
import android.util.ArraySet;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * Class that listens to camera open/closed signals, keeps track of the current apps using camera,
 * and notifies listeners.
 */
class CameraStateMonitor {
    private static final String TAG = TAG_WITH_CLASS_NAME ? "CameraStateMonitor" : TAG_WM;

    // Delay for updating letterbox after Camera connection is closed. Needed to avoid flickering
    // when an app is flipping between front and rear cameras or when size compat mode is restarted.
    // TODO(b/330148095): Investigate flickering without using delays, remove them if possible.
@@ -53,6 +46,7 @@ class CameraStateMonitor {
    private static final int CAMERA_OPENED_LETTERBOX_UPDATE_DELAY_MS =
            CAMERA_CLOSED_LETTERBOX_UPDATE_DELAY_MS / 2;

    /** Returns the information about apps using camera, for logging purposes. */
    @NonNull
    private final DisplayContent mDisplayContent;
    @NonNull
@@ -62,20 +56,8 @@ class CameraStateMonitor {
    @NonNull
    private final Handler mHandler;

    // Bi-directional map between package names and active camera IDs since we need to 1) get a
    // camera id by a package name when resizing the window; 2) get a package name by a camera id
    // when camera connection is closed and we need to clean up our records.
    private final CameraIdPackageNameBiMapping mCameraIdPackageBiMapping =
            new CameraIdPackageNameBiMapping();
    // TODO(b/380840084): Consider making this a set of CameraId/PackageName pairs. This is to
    // keep track of camera-closed signals when apps are switching camera access, so that the policy
    // can restore app configuration when an app closes camera (e.g. loses camera access due to
    // another app).
    private final Set<String> mScheduledToBeRemovedCameraIdSet = new ArraySet<>();

    // TODO(b/336474959): should/can this go in the compat listeners?
    private final Set<String> mScheduledCompatModeUpdateCameraIdSet = new ArraySet<>();

    @VisibleForTesting
    final AppCompatCameraStateStrategy mAppCompatCameraStateStrategy;
    @VisibleForTesting
    final AppCompatCameraStatePolicy mAppCompatCameraStatePolicy;

@@ -110,6 +92,7 @@ class CameraStateMonitor {
        mAppCompatCameraStatePolicy = appCompatCameraStatePolicy;
        mWmService = displayContent.mWmService;
        mCameraManager = mWmService.mContext.getSystemService(CameraManager.class);
        mAppCompatCameraStateStrategy = new AppCompatCameraStateStrategyForPackage(displayContent);
    }

    /** Starts listening to camera opened/closed signals. */
@@ -122,7 +105,7 @@ class CameraStateMonitor {
    }

    /** Stops listening to camera opened/closed signals. */
    public void stopListeningToCameraState() {
    void stopListeningToCameraState() {
        if (mCameraManager != null) {
            mCameraManager.unregisterAvailabilityCallback(mAvailabilityCallback);
        }
@@ -140,37 +123,23 @@ class CameraStateMonitor {

    private void notifyCameraOpenedWithDelay(@NonNull String cameraId,
            @NonNull String packageName) {
        // Some apps can’t handle configuration changes coming at the same time with Camera setup so
        // delaying orientation update to accommodate for that.
        // If an activity is restarting or camera is flipping, the camera connection can be
        // quickly closed and reopened.
        mScheduledToBeRemovedCameraIdSet.remove(cameraId);
        ProtoLog.v(WM_DEBUG_STATES,
                "Display id=%d is notified that Camera %s is open for package %s",
                mDisplayContent.mDisplayId, cameraId, packageName);
        // Some apps can’t handle configuration changes coming at the same time with Camera setup so
        // delaying orientation update to accommodate for that.
        mScheduledCompatModeUpdateCameraIdSet.add(cameraId);
        mHandler.postDelayed(() -> notifyCameraOpenedInternal(cameraId, packageName),
                CAMERA_OPENED_LETTERBOX_UPDATE_DELAY_MS);
        mAppCompatCameraStateStrategy.trackOnCameraOpened(cameraId);
        mHandler.postDelayed(() -> {
            synchronized (mWmService.mGlobalLock) {
                notifyCameraOpenedInternal(cameraId, packageName);
            }}, CAMERA_OPENED_LETTERBOX_UPDATE_DELAY_MS);
    }

    private void notifyCameraOpenedInternal(@NonNull String cameraId, @NonNull String packageName) {
        synchronized (mWmService.mGlobalLock) {
            if (!mScheduledCompatModeUpdateCameraIdSet.remove(cameraId)) {
                // Camera compat mode update has happened already or was cancelled
                // because camera was closed.
                return;
            }
            mCameraIdPackageBiMapping.put(packageName, cameraId);
            // If there are multiple activities of the same package name and none of
            // them are the top running activity, we do not apply treatment (rather than
            // guessing and applying it to the wrong activity).
            final ActivityRecord cameraActivity =
                    findUniqueActivityWithPackageName(packageName);
            if (cameraActivity == null || cameraActivity.getTask() == null) {
                return;
            }
            mAppCompatCameraStatePolicy.onCameraOpened(cameraActivity);
        }
        mAppCompatCameraStateStrategy.notifyPolicyCameraOpenedIfNeeded(cameraId, packageName,
                mAppCompatCameraStatePolicy);
    }

    /**
@@ -183,99 +152,44 @@ class CameraStateMonitor {
        ProtoLog.v(WM_DEBUG_STATES,
                "Display id=%d is notified that Camera %s is closed.",
                mDisplayContent.mDisplayId, cameraId);
        mScheduledToBeRemovedCameraIdSet.add(cameraId);
        // No need to update window size for this camera if it's already closed.
        mScheduledCompatModeUpdateCameraIdSet.remove(cameraId);
        scheduleRemoveCameraId(cameraId);
    }

    /** Returns whether a given activity holds any camera opened. */
    boolean isCameraRunningForActivity(@NonNull ActivityRecord activity) {
        return getCameraIdForActivity(activity) != null;
        return mAppCompatCameraStateStrategy.isCameraRunningForActivity(activity);
    }

    /** Returns whether a given activity holds a specific camera opened. */
    // TODO(b/336474959): try to decouple `cameraId` from the listeners.
    boolean isCameraWithIdRunningForActivity(@NonNull ActivityRecord activity, String cameraId) {
        return cameraId.equals(getCameraIdForActivity(activity));
    }

    void rescheduleRemoveCameraActivity(@NonNull String cameraId) {
        mScheduledToBeRemovedCameraIdSet.add(cameraId);
        scheduleRemoveCameraId(cameraId);
    }

    @Nullable
    private String getCameraIdForActivity(@NonNull ActivityRecord activity) {
        return mCameraIdPackageBiMapping.getCameraId(activity.packageName);
    boolean isCameraWithIdRunningForActivity(@NonNull ActivityRecord activity,
            @NonNull String cameraId) {
        return mAppCompatCameraStateStrategy.isCameraWithIdRunningForActivity(activity, cameraId);
    }

    // Delay is needed to avoid rotation flickering when an app is flipping between front and
    // rear cameras, when size compat mode is restarted or activity is being refreshed.
    private void scheduleRemoveCameraId(@NonNull String cameraId) {
        mHandler.postDelayed(
                () -> removeCameraId(cameraId),
                CAMERA_CLOSED_LETTERBOX_UPDATE_DELAY_MS);
        mAppCompatCameraStateStrategy.trackOnCameraClosed(cameraId);
        mHandler.postDelayed(() ->  {
            synchronized (mWmService.mGlobalLock) {
                removeCameraId(cameraId);
            }}, CAMERA_CLOSED_LETTERBOX_UPDATE_DELAY_MS);
    }

    private void removeCameraId(@NonNull String cameraId) {
        synchronized (mWmService.mGlobalLock) {
            if (!mScheduledToBeRemovedCameraIdSet.remove(cameraId)) {
                // Already reconnected to this camera, no need to clean up.
                return;
            }
            final boolean canClose = mAppCompatCameraStatePolicy.canCameraBeClosed(cameraId);
            if (canClose) {
                // Finish cleaning up.
                mCameraIdPackageBiMapping.removeCameraId(cameraId);
                mAppCompatCameraStatePolicy.onCameraClosed();
            } else {
        final boolean completed = mAppCompatCameraStateStrategy.notifyPolicyCameraClosedIfNeeded(
                cameraId, mAppCompatCameraStatePolicy);
        if (!completed) {
            // Not ready to process closure yet - the camera activity might be refreshing.
            // Try again later.
                rescheduleRemoveCameraActivity(cameraId);
            }
        }
    }

    // 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()) {
            Slog.w(TAG, "Cannot find camera activity.");
            return null;
        }

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

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

    String getSummary() {
        return " CameraIdPackageNameBiMapping="
                + mCameraIdPackageBiMapping
                .getSummaryForDisplayRotationHistoryRecord();
    @NonNull
    @Override
    public String toString() {
        return mAppCompatCameraStateStrategy.toString();
    }
}
+1 −2
Original line number Diff line number Diff line
@@ -211,8 +211,7 @@ final class DisplayRotationCompatPolicy implements AppCompatCameraStatePolicy,
                            + (topActivity == null ? "null" : topActivity.shortComponentName)
                    + " isTreatmentEnabledForActivity="
                            + isTreatmentEnabledForActivity(topActivity)
                            + "mCameraStateMonitor="
                            + mCameraStateMonitor.getSummary();
                            + "mCameraStateMonitor=" + mCameraStateMonitor;
        }
        return "DisplayRotationCompatPolicy{"
                + " isTreatmentEnabledForDisplay=" + isTreatmentEnabledForDisplay()
Loading