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