1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.server.wm;
17 
18 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
19 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
20 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.hardware.camera2.CameraManager;
25 import android.os.Handler;
26 import android.util.ArraySet;
27 import android.util.Slog;
28 
29 import com.android.internal.protolog.common.ProtoLog;
30 
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Set;
34 
35 /**
36  * Class that listens to camera open/closed signals, keeps track of the current apps using camera,
37  * and notifies listeners.
38  */
39 class CameraStateMonitor {
40     private static final String TAG = TAG_WITH_CLASS_NAME ? "CameraStateMonitor" : TAG_WM;
41 
42     // Delay for updating letterbox after Camera connection is closed. Needed to avoid flickering
43     // when an app is flipping between front and rear cameras or when size compat mode is restarted.
44     // TODO(b/330148095): Investigate flickering without using delays, remove them if possible.
45     private static final int CAMERA_CLOSED_LETTERBOX_UPDATE_DELAY_MS = 2000;
46     // Delay for updating letterboxing after Camera connection is opened. This delay is selected to
47     // be long enough to avoid conflicts with transitions on the app's side.
48     // Using a delay < CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS to avoid flickering when an app
49     // is flipping between front and rear cameras (in case requested orientation changes at
50     // runtime at the same time) or when size compat mode is restarted.
51     // TODO(b/330148095): Investigate flickering without using delays, remove them if possible.
52     private static final int CAMERA_OPENED_LETTERBOX_UPDATE_DELAY_MS =
53             CAMERA_CLOSED_LETTERBOX_UPDATE_DELAY_MS / 2;
54 
55     @NonNull
56     private final DisplayContent mDisplayContent;
57     @NonNull
58     private final WindowManagerService mWmService;
59     @Nullable
60     private final CameraManager mCameraManager;
61     @NonNull
62     private final Handler mHandler;
63 
64     @Nullable
65     private ActivityRecord mCameraActivity;
66 
67     // Bi-directional map between package names and active camera IDs since we need to 1) get a
68     // camera id by a package name when resizing the window; 2) get a package name by a camera id
69     // when camera connection is closed and we need to clean up our records.
70     private final CameraIdPackageNameBiMapping mCameraIdPackageBiMapping =
71             new CameraIdPackageNameBiMapping();
72     private final Set<String> mScheduledToBeRemovedCameraIdSet = new ArraySet<>();
73 
74     // TODO(b/336474959): should/can this go in the compat listeners?
75     private final Set<String> mScheduledCompatModeUpdateCameraIdSet = new ArraySet<>();
76 
77     private final ArrayList<CameraCompatStateListener> mCameraStateListeners = new ArrayList<>();
78 
79     /**
80      * {@link CameraCompatStateListener} which returned {@code true} on the last {@link
81      * CameraCompatStateListener#onCameraOpened(ActivityRecord, String)}, if any.
82      *
83      * <p>This allows the {@link CameraStateMonitor} to notify a particular listener when camera
84      * closes, so they can revert any changes.
85      */
86     @Nullable
87     private CameraCompatStateListener mCurrentListenerForCameraActivity;
88 
89     private final CameraManager.AvailabilityCallback mAvailabilityCallback =
90             new  CameraManager.AvailabilityCallback() {
91                 @Override
92                 public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) {
93                     synchronized (mWmService.mGlobalLock) {
94                         notifyCameraOpened(cameraId, packageId);
95                     }
96                 }
97                 @Override
98                 public void onCameraClosed(@NonNull String cameraId) {
99                     synchronized (mWmService.mGlobalLock) {
100                         notifyCameraClosed(cameraId);
101                     }
102                 }
103             };
104 
CameraStateMonitor(@onNull DisplayContent displayContent, @NonNull Handler handler)105     CameraStateMonitor(@NonNull DisplayContent displayContent, @NonNull Handler handler) {
106         // This constructor is called from DisplayContent constructor. Don't use any fields in
107         // DisplayContent here since they aren't guaranteed to be set.
108         mHandler = handler;
109         mDisplayContent = displayContent;
110         mWmService = displayContent.mWmService;
111         mCameraManager = mWmService.mContext.getSystemService(CameraManager.class);
112     }
113 
startListeningToCameraState()114     void startListeningToCameraState() {
115         mCameraManager.registerAvailabilityCallback(
116                 mWmService.mContext.getMainExecutor(), mAvailabilityCallback);
117     }
118 
119     /** Releases camera callback listener. */
dispose()120     void dispose() {
121         if (mCameraManager != null) {
122             mCameraManager.unregisterAvailabilityCallback(mAvailabilityCallback);
123         }
124     }
125 
addCameraStateListener(CameraCompatStateListener listener)126     void addCameraStateListener(CameraCompatStateListener listener) {
127         mCameraStateListeners.add(listener);
128     }
129 
removeCameraStateListener(CameraCompatStateListener listener)130     void removeCameraStateListener(CameraCompatStateListener listener) {
131         mCameraStateListeners.remove(listener);
132     }
133 
notifyCameraOpened( @onNull String cameraId, @NonNull String packageName)134     private void notifyCameraOpened(
135             @NonNull String cameraId, @NonNull String packageName) {
136         // If an activity is restarting or camera is flipping, the camera connection can be
137         // quickly closed and reopened.
138         mScheduledToBeRemovedCameraIdSet.remove(cameraId);
139         ProtoLog.v(WM_DEBUG_STATES,
140                 "Display id=%d is notified that Camera %s is open for package %s",
141                 mDisplayContent.mDisplayId, cameraId, packageName);
142         // Some apps can’t handle configuration changes coming at the same time with Camera setup so
143         // delaying orientation update to accommodate for that.
144         mScheduledCompatModeUpdateCameraIdSet.add(cameraId);
145         mHandler.postDelayed(
146                 () -> {
147                     synchronized (mWmService.mGlobalLock) {
148                         if (!mScheduledCompatModeUpdateCameraIdSet.remove(cameraId)) {
149                             // Camera compat mode update has happened already or was cancelled
150                             // because camera was closed.
151                             return;
152                         }
153                         mCameraIdPackageBiMapping.put(packageName, cameraId);
154                         mCameraActivity = findCameraActivity(packageName);
155                         if (mCameraActivity == null || mCameraActivity.getTask() == null) {
156                             return;
157                         }
158                         notifyListenersCameraOpened(mCameraActivity, cameraId);
159                     }
160                 },
161                 CAMERA_OPENED_LETTERBOX_UPDATE_DELAY_MS);
162     }
163 
notifyListenersCameraOpened(@onNull ActivityRecord cameraActivity, @NonNull String cameraId)164     private void notifyListenersCameraOpened(@NonNull ActivityRecord cameraActivity,
165             @NonNull String cameraId) {
166         for (int i = 0; i < mCameraStateListeners.size(); i++) {
167             CameraCompatStateListener listener = mCameraStateListeners.get(i);
168             boolean activeCameraTreatment = listener.onCameraOpened(
169                     cameraActivity, cameraId);
170             if (activeCameraTreatment) {
171                 mCurrentListenerForCameraActivity = listener;
172                 break;
173             }
174         }
175     }
176 
notifyCameraClosed(@onNull String cameraId)177     private void notifyCameraClosed(@NonNull String cameraId) {
178         ProtoLog.v(WM_DEBUG_STATES,
179                 "Display id=%d is notified that Camera %s is closed.",
180                 mDisplayContent.mDisplayId, cameraId);
181         mScheduledToBeRemovedCameraIdSet.add(cameraId);
182         // No need to update window size for this camera if it's already closed.
183         mScheduledCompatModeUpdateCameraIdSet.remove(cameraId);
184         scheduleRemoveCameraId(cameraId);
185     }
186 
isCameraRunningForActivity(@onNull ActivityRecord activity)187     boolean isCameraRunningForActivity(@NonNull ActivityRecord activity) {
188         return getCameraIdForActivity(activity) != null;
189     }
190 
191     // TODO(b/336474959): try to decouple `cameraId` from the listeners.
isCameraWithIdRunningForActivity(@onNull ActivityRecord activity, String cameraId)192     boolean isCameraWithIdRunningForActivity(@NonNull ActivityRecord activity, String cameraId) {
193         return cameraId.equals(getCameraIdForActivity(activity));
194     }
195 
rescheduleRemoveCameraActivity(@onNull String cameraId)196     void rescheduleRemoveCameraActivity(@NonNull String cameraId) {
197         mScheduledToBeRemovedCameraIdSet.add(cameraId);
198         scheduleRemoveCameraId(cameraId);
199     }
200 
201     @Nullable
getCameraIdForActivity(@onNull ActivityRecord activity)202     private String getCameraIdForActivity(@NonNull ActivityRecord activity) {
203         return mCameraIdPackageBiMapping.getCameraId(activity.packageName);
204     }
205 
206     // Delay is needed to avoid rotation flickering when an app is flipping between front and
207     // rear cameras, when size compat mode is restarted or activity is being refreshed.
scheduleRemoveCameraId(@onNull String cameraId)208     private void scheduleRemoveCameraId(@NonNull String cameraId) {
209         mHandler.postDelayed(
210                 () -> removeCameraId(cameraId),
211                 CAMERA_CLOSED_LETTERBOX_UPDATE_DELAY_MS);
212     }
213 
removeCameraId(@onNull String cameraId)214     private void removeCameraId(@NonNull String cameraId) {
215         synchronized (mWmService.mGlobalLock) {
216             if (!mScheduledToBeRemovedCameraIdSet.remove(cameraId)) {
217                 // Already reconnected to this camera, no need to clean up.
218                 return;
219             }
220             if (mCameraActivity != null && mCurrentListenerForCameraActivity != null) {
221                 boolean closeSuccessful =
222                         mCurrentListenerForCameraActivity.onCameraClosed(mCameraActivity, cameraId);
223                 if (closeSuccessful) {
224                     mCameraIdPackageBiMapping.removeCameraId(cameraId);
225                     mCurrentListenerForCameraActivity = null;
226                 } else {
227                     rescheduleRemoveCameraActivity(cameraId);
228                 }
229             }
230         }
231     }
232 
233     // TODO(b/335165310): verify that this works in multi instance and permission dialogs.
234     @Nullable
findCameraActivity(@onNull String packageName)235     private ActivityRecord findCameraActivity(@NonNull String packageName) {
236         final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
237                 /* considerKeyguardState= */ true);
238         if (topActivity != null && topActivity.packageName.equals(packageName)) {
239             return topActivity;
240         }
241 
242         final List<ActivityRecord> activitiesOfPackageWhichOpenedCamera = new ArrayList<>();
243         mDisplayContent.forAllActivities(activityRecord -> {
244             if (activityRecord.isVisibleRequested()
245                     && activityRecord.packageName.equals(packageName)) {
246                 activitiesOfPackageWhichOpenedCamera.add(activityRecord);
247             }
248         });
249 
250         if (activitiesOfPackageWhichOpenedCamera.isEmpty()) {
251             Slog.w(TAG, "Cannot find camera activity.");
252             return null;
253         }
254 
255         if (activitiesOfPackageWhichOpenedCamera.size() == 1) {
256             return activitiesOfPackageWhichOpenedCamera.getFirst();
257         }
258 
259         // Return null if we cannot determine which activity opened camera. This is preferred to
260         // applying treatment to the wrong activity.
261         Slog.w(TAG, "Cannot determine which activity opened camera.");
262         return null;
263     }
264 
getSummary()265     String getSummary() {
266         return " CameraIdPackageNameBiMapping="
267                 + mCameraIdPackageBiMapping
268                 .getSummaryForDisplayRotationHistoryRecord();
269     }
270 
271     interface CameraCompatStateListener {
272         /**
273          * Notifies the compat listener that an activity has opened camera.
274          *
275          * @return true if the treatment has been applied.
276          */
277         // TODO(b/336474959): try to decouple `cameraId` from the listeners.
onCameraOpened(@onNull ActivityRecord cameraActivity, @NonNull String cameraId)278         boolean onCameraOpened(@NonNull ActivityRecord cameraActivity, @NonNull String cameraId);
279         /**
280          * Notifies the compat listener that an activity has closed the camera.
281          *
282          * @return true if cleanup has been successful - the notifier might try again if false.
283          */
284         // TODO(b/336474959): try to decouple `cameraId` from the listeners.
onCameraClosed(@onNull ActivityRecord cameraActivity, @NonNull String cameraId)285         boolean onCameraClosed(@NonNull ActivityRecord cameraActivity, @NonNull String cameraId);
286     }
287 }
288