1 /*
2  * Copyright (C) 2022 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 
17 package com.android.server.wm;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
21 import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
22 import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
23 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
24 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
25 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
26 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
27 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
28 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
29 import static android.content.pm.ActivityInfo.screenOrientationToString;
30 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
31 import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
32 import static android.view.Display.TYPE_INTERNAL;
33 
34 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
35 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
36 import static com.android.server.wm.DisplayRotationReversionController.REVERSION_TYPE_CAMERA_COMPAT;
37 
38 import android.annotation.NonNull;
39 import android.annotation.Nullable;
40 import android.annotation.StringRes;
41 import android.app.servertransaction.ClientTransaction;
42 import android.app.servertransaction.RefreshCallbackItem;
43 import android.app.servertransaction.ResumeActivityItem;
44 import android.content.pm.ActivityInfo.ScreenOrientation;
45 import android.content.pm.PackageManager;
46 import android.content.res.Configuration;
47 import android.hardware.camera2.CameraManager;
48 import android.os.Handler;
49 import android.os.RemoteException;
50 import android.util.ArrayMap;
51 import android.util.ArraySet;
52 import android.widget.Toast;
53 
54 import com.android.internal.R;
55 import com.android.internal.annotations.GuardedBy;
56 import com.android.internal.annotations.VisibleForTesting;
57 import com.android.internal.protolog.common.ProtoLog;
58 import com.android.server.UiThread;
59 
60 import java.util.Map;
61 import java.util.Set;
62 
63 /**
64  * Controls camera compatibility treatment that handles orientation mismatch between camera
65  * buffers and an app window for a particular display that can lead to camera issues like sideways
66  * or stretched viewfinder.
67  *
68  * <p>This includes force rotation of fixed orientation activities connected to the camera.
69  *
70  * <p>The treatment is enabled for internal displays that have {@code ignoreOrientationRequest}
71  * display setting enabled and when {@code
72  * R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}.
73  */
74  // TODO(b/261444714): Consider moving Camera-specific logic outside of the WM Core path
75 final class DisplayRotationCompatPolicy {
76 
77     // Delay for updating display rotation after Camera connection is closed. Needed to avoid
78     // rotation flickering when an app is flipping between front and rear cameras or when size
79     // compat mode is restarted.
80     // TODO(b/263114289): Consider associating this delay with a specific activity so that if
81     // the new non-camera activity started on top of the camer one we can rotate faster.
82     private static final int CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS = 2000;
83     // Delay for updating display rotation after Camera connection is opened. This delay is
84     // selected to be long enough to avoid conflicts with transitions on the app's side.
85     // Using a delay < CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS to avoid flickering when an app
86     // is flipping between front and rear cameras (in case requested orientation changes at
87     // runtime at the same time) or when size compat mode is restarted.
88     private static final int CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS =
89             CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS / 2;
90     // Delay for ensuring that onActivityRefreshed is always called after an activity refresh. The
91     // client process may not always report the event back to the server, such as process is
92     // crashed or got killed.
93     private static final int REFRESH_CALLBACK_TIMEOUT_MS = 2000;
94 
95     private final DisplayContent mDisplayContent;
96     private final WindowManagerService mWmService;
97     private final CameraManager mCameraManager;
98     private final Handler mHandler;
99 
100     // Bi-directional map between package names and active camera IDs since we need to 1) get a
101     // camera id by a package name when determining rotation; 2) get a package name by a camera id
102     // when camera connection is closed and we need to clean up our records.
103     @GuardedBy("this")
104     private final CameraIdPackageNameBiMap mCameraIdPackageBiMap = new CameraIdPackageNameBiMap();
105     @GuardedBy("this")
106     private final Set<String> mScheduledToBeRemovedCameraIdSet = new ArraySet<>();
107     @GuardedBy("this")
108     private final Set<String> mScheduledOrientationUpdateCameraIdSet = new ArraySet<>();
109 
110     private final CameraManager.AvailabilityCallback mAvailabilityCallback =
111             new  CameraManager.AvailabilityCallback() {
112                 @Override
113                 public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) {
114                     notifyCameraOpened(cameraId, packageId);
115                 }
116 
117                 @Override
118                 public void onCameraClosed(@NonNull String cameraId) {
119                     notifyCameraClosed(cameraId);
120                 }
121             };
122 
123     @ScreenOrientation
124     private int mLastReportedOrientation = SCREEN_ORIENTATION_UNSET;
125 
DisplayRotationCompatPolicy(@onNull DisplayContent displayContent)126     DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent) {
127         this(displayContent, displayContent.mWmService.mH);
128     }
129 
130     @VisibleForTesting
DisplayRotationCompatPolicy(@onNull DisplayContent displayContent, Handler handler)131     DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent, Handler handler) {
132         // This constructor is called from DisplayContent constructor. Don't use any fields in
133         // DisplayContent here since they aren't guaranteed to be set.
134         mHandler = handler;
135         mDisplayContent = displayContent;
136         mWmService = displayContent.mWmService;
137         mCameraManager = mWmService.mContext.getSystemService(CameraManager.class);
138         mCameraManager.registerAvailabilityCallback(
139                 mWmService.mContext.getMainExecutor(), mAvailabilityCallback);
140     }
141 
dispose()142     void dispose() {
143         mCameraManager.unregisterAvailabilityCallback(mAvailabilityCallback);
144     }
145 
146     /**
147      * Determines orientation for Camera compatibility.
148      *
149      * <p>The goal of this function is to compute a orientation which would align orientations of
150      * portrait app window and natural orientation of the device and set opposite to natural
151      * orientation for a landscape app window. This is one of the strongest assumptions that apps
152      * make when they implement camera previews. Since app and natural display orientations aren't
153      * guaranteed to match, the rotation can cause letterboxing.
154      *
155      * <p>If treatment isn't applicable returns {@link SCREEN_ORIENTATION_UNSPECIFIED}. See {@link
156      * #shouldComputeCameraCompatOrientation} for conditions enabling the treatment.
157      */
158     @ScreenOrientation
getOrientation()159     int getOrientation() {
160         mLastReportedOrientation = getOrientationInternal();
161         if (mLastReportedOrientation != SCREEN_ORIENTATION_UNSPECIFIED) {
162             rememberOverriddenOrientationIfNeeded();
163         } else {
164             restoreOverriddenOrientationIfNeeded();
165         }
166         return mLastReportedOrientation;
167     }
168 
169     @ScreenOrientation
getOrientationInternal()170     private synchronized int getOrientationInternal() {
171         if (!isTreatmentEnabledForDisplay()) {
172             return SCREEN_ORIENTATION_UNSPECIFIED;
173         }
174         ActivityRecord topActivity = mDisplayContent.topRunningActivity(
175                 /* considerKeyguardState= */ true);
176         if (!isTreatmentEnabledForActivity(topActivity)) {
177             return SCREEN_ORIENTATION_UNSPECIFIED;
178         }
179         boolean isPortraitActivity =
180                 topActivity.getRequestedConfigurationOrientation() == ORIENTATION_PORTRAIT;
181         boolean isNaturalDisplayOrientationPortrait =
182                 mDisplayContent.getNaturalOrientation() == ORIENTATION_PORTRAIT;
183         // Rotate portrait-only activity in the natural orientation of the displays (and in the
184         // opposite to natural orientation for landscape-only) since many apps assume that those
185         // are aligned when they compute orientation of the preview.
186         // This means that even for a landscape-only activity and a device with landscape natural
187         // orientation this would return SCREEN_ORIENTATION_PORTRAIT because an assumption that
188         // natural orientation = portrait window = portait camera is the main wrong assumption
189         // that apps make when they implement camera previews so landscape windows need be
190         // rotated in the orientation oposite to the natural one even if it's portrait.
191         // TODO(b/261475895): Consider allowing more rotations for "sensor" and "user" versions
192         // of the portrait and landscape orientation requests.
193         int orientation = (isPortraitActivity && isNaturalDisplayOrientationPortrait)
194                 || (!isPortraitActivity && !isNaturalDisplayOrientationPortrait)
195                         ? SCREEN_ORIENTATION_PORTRAIT
196                         : SCREEN_ORIENTATION_LANDSCAPE;
197         ProtoLog.v(WM_DEBUG_ORIENTATION,
198                 "Display id=%d is ignoring all orientation requests, camera is active "
199                         + "and the top activity is eligible for force rotation, return %s,"
200                         + "portrait activity: %b, is natural orientation portrait: %b.",
201                 mDisplayContent.mDisplayId, screenOrientationToString(orientation),
202                 isPortraitActivity, isNaturalDisplayOrientationPortrait);
203         return orientation;
204     }
205 
206     /**
207      * "Refreshes" activity by going through "stopped -> resumed" or "paused -> resumed" cycle.
208      * This allows to clear cached values in apps (e.g. display or camera rotation) that influence
209      * camera preview and can lead to sideways or stretching issues persisting even after force
210      * rotation.
211      */
onActivityConfigurationChanging(ActivityRecord activity, Configuration newConfig, Configuration lastReportedConfig)212     void onActivityConfigurationChanging(ActivityRecord activity, Configuration newConfig,
213             Configuration lastReportedConfig) {
214         if (!isTreatmentEnabledForDisplay()
215                 || !mWmService.mLetterboxConfiguration.isCameraCompatRefreshEnabled()
216                 || !shouldRefreshActivity(activity, newConfig, lastReportedConfig)) {
217             return;
218         }
219         boolean cycleThroughStop =
220                 mWmService.mLetterboxConfiguration
221                         .isCameraCompatRefreshCycleThroughStopEnabled()
222                 && !activity.mLetterboxUiController
223                         .shouldRefreshActivityViaPauseForCameraCompat();
224         try {
225             activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true);
226             ProtoLog.v(WM_DEBUG_STATES,
227                     "Refreshing activity for camera compatibility treatment, "
228                             + "activityRecord=%s", activity);
229             final ClientTransaction transaction = ClientTransaction.obtain(
230                     activity.app.getThread(), activity.token);
231             transaction.addCallback(
232                     RefreshCallbackItem.obtain(cycleThroughStop ? ON_STOP : ON_PAUSE));
233             transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(
234                     /* isForward */ false, /* shouldSendCompatFakeFocus */ false));
235             activity.mAtmService.getLifecycleManager().scheduleTransaction(transaction);
236             mHandler.postDelayed(
237                     () -> onActivityRefreshed(activity),
238                     REFRESH_CALLBACK_TIMEOUT_MS);
239         } catch (RemoteException e) {
240             activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false);
241         }
242     }
243 
onActivityRefreshed(@onNull ActivityRecord activity)244     void onActivityRefreshed(@NonNull ActivityRecord activity) {
245         activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false);
246     }
247 
248     /**
249      * Notifies that animation in {@link ScreenRotationAnimation} has finished.
250      *
251      * <p>This class uses this signal as a trigger for notifying the user about forced rotation
252      * reason with the {@link Toast}.
253      */
onScreenRotationAnimationFinished()254     void onScreenRotationAnimationFinished() {
255         if (!isTreatmentEnabledForDisplay() || mCameraIdPackageBiMap.isEmpty()) {
256             return;
257         }
258         ActivityRecord topActivity = mDisplayContent.topRunningActivity(
259                     /* considerKeyguardState= */ true);
260         if (!isTreatmentEnabledForActivity(topActivity)) {
261             return;
262         }
263         showToast(R.string.display_rotation_camera_compat_toast_after_rotation);
264     }
265 
getSummaryForDisplayRotationHistoryRecord()266     String getSummaryForDisplayRotationHistoryRecord() {
267         String summaryIfEnabled = "";
268         if (isTreatmentEnabledForDisplay()) {
269             ActivityRecord topActivity = mDisplayContent.topRunningActivity(
270                     /* considerKeyguardState= */ true);
271             summaryIfEnabled =
272                     " mLastReportedOrientation="
273                             + screenOrientationToString(mLastReportedOrientation)
274                     + " topActivity="
275                             + (topActivity == null ? "null" : topActivity.shortComponentName)
276                     + " isTreatmentEnabledForActivity="
277                             + isTreatmentEnabledForActivity(topActivity)
278                     + " CameraIdPackageNameBiMap="
279                             + mCameraIdPackageBiMap.getSummaryForDisplayRotationHistoryRecord();
280         }
281         return "DisplayRotationCompatPolicy{"
282                 + " isTreatmentEnabledForDisplay=" + isTreatmentEnabledForDisplay()
283                 + summaryIfEnabled
284                 + " }";
285     }
286 
restoreOverriddenOrientationIfNeeded()287     private void restoreOverriddenOrientationIfNeeded() {
288         if (!isOrientationOverridden()) {
289             return;
290         }
291         if (mDisplayContent.getRotationReversionController().revertOverride(
292                 REVERSION_TYPE_CAMERA_COMPAT)) {
293             ProtoLog.v(WM_DEBUG_ORIENTATION,
294                     "Reverting orientation after camera compat force rotation");
295             // Reset last orientation source since we have reverted the orientation.
296             mDisplayContent.mLastOrientationSource = null;
297         }
298     }
299 
isOrientationOverridden()300     private boolean isOrientationOverridden() {
301         return mDisplayContent.getRotationReversionController().isOverrideActive(
302                 REVERSION_TYPE_CAMERA_COMPAT);
303     }
304 
rememberOverriddenOrientationIfNeeded()305     private void rememberOverriddenOrientationIfNeeded() {
306         if (!isOrientationOverridden()) {
307             mDisplayContent.getRotationReversionController().beforeOverrideApplied(
308                     REVERSION_TYPE_CAMERA_COMPAT);
309             ProtoLog.v(WM_DEBUG_ORIENTATION,
310                     "Saving original orientation before camera compat, last orientation is %d",
311                     mDisplayContent.getLastOrientation());
312         }
313     }
314 
315     // Refreshing only when configuration changes after rotation or camera split screen aspect ratio
316     // treatment is enabled
shouldRefreshActivity(ActivityRecord activity, Configuration newConfig, Configuration lastReportedConfig)317     private boolean shouldRefreshActivity(ActivityRecord activity, Configuration newConfig,
318             Configuration lastReportedConfig) {
319         final boolean displayRotationChanged = (newConfig.windowConfiguration.getDisplayRotation()
320                 != lastReportedConfig.windowConfiguration.getDisplayRotation());
321         return (displayRotationChanged
322                 || activity.mLetterboxUiController.isCameraCompatSplitScreenAspectRatioAllowed())
323                 && isTreatmentEnabledForActivity(activity)
324                 && activity.mLetterboxUiController.shouldRefreshActivityForCameraCompat();
325     }
326 
327     /**
328      * Whether camera compat treatment is enabled for the display.
329      *
330      * <p>Conditions that need to be met:
331      * <ul>
332      *     <li>{@code R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}.
333      *     <li>Setting {@code ignoreOrientationRequest} is enabled for the display.
334      *     <li>Associated {@link DisplayContent} is for internal display. See b/225928882
335      *     that tracks supporting external displays in the future.
336      * </ul>
337      */
isTreatmentEnabledForDisplay()338     private boolean isTreatmentEnabledForDisplay() {
339         return mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled()
340                 && mDisplayContent.getIgnoreOrientationRequest()
341                 // TODO(b/225928882): Support camera compat rotation for external displays
342                 && mDisplayContent.getDisplay().getType() == TYPE_INTERNAL;
343     }
344 
isActivityEligibleForOrientationOverride(@onNull ActivityRecord activity)345     boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) {
346         return isTreatmentEnabledForDisplay()
347                 && isCameraActive(activity, /* mustBeFullscreen */ true);
348     }
349 
350 
351     /**
352      * Whether camera compat treatment is applicable for the given activity.
353      *
354      * <p>Conditions that need to be met:
355      * <ul>
356      *     <li>Camera is active for the package.
357      *     <li>The activity is in fullscreen
358      *     <li>The activity has fixed orientation but not "locked" or "nosensor" one.
359      * </ul>
360      */
isTreatmentEnabledForActivity(@ullable ActivityRecord activity)361     boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
362         return isTreatmentEnabledForActivity(activity, /* mustBeFullscreen */ true);
363     }
364 
isTreatmentEnabledForActivity(@ullable ActivityRecord activity, boolean mustBeFullscreen)365     private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity,
366             boolean mustBeFullscreen) {
367         return activity != null && isCameraActive(activity, mustBeFullscreen)
368                 && activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED
369                 // "locked" and "nosensor" values are often used by camera apps that can't
370                 // handle dynamic changes so we shouldn't force rotate them.
371                 && activity.getOverrideOrientation() != SCREEN_ORIENTATION_NOSENSOR
372                 && activity.getOverrideOrientation() != SCREEN_ORIENTATION_LOCKED;
373     }
374 
isCameraActive(@onNull ActivityRecord activity, boolean mustBeFullscreen)375     private boolean isCameraActive(@NonNull ActivityRecord activity, boolean mustBeFullscreen) {
376         // Checking windowing mode on activity level because we don't want to
377         // apply treatment in case of activity embedding.
378         return (!mustBeFullscreen || !activity.inMultiWindowMode())
379                 && mCameraIdPackageBiMap.containsPackageName(activity.packageName)
380                 && activity.mLetterboxUiController.shouldForceRotateForCameraCompat();
381     }
382 
notifyCameraOpened( @onNull String cameraId, @NonNull String packageName)383     private synchronized void notifyCameraOpened(
384             @NonNull String cameraId, @NonNull String packageName) {
385         // If an activity is restarting or camera is flipping, the camera connection can be
386         // quickly closed and reopened.
387         mScheduledToBeRemovedCameraIdSet.remove(cameraId);
388         ProtoLog.v(WM_DEBUG_ORIENTATION,
389                 "Display id=%d is notified that Camera %s is open for package %s",
390                 mDisplayContent.mDisplayId, cameraId, packageName);
391         // Some apps can’t handle configuration changes coming at the same time with Camera setup
392         // so delaying orientation update to accomadate for that.
393         mScheduledOrientationUpdateCameraIdSet.add(cameraId);
394         mHandler.postDelayed(
395                 () ->  delayedUpdateOrientationWithWmLock(cameraId, packageName),
396                 CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS);
397     }
398 
delayedUpdateOrientationWithWmLock( @onNull String cameraId, @NonNull String packageName)399     private void delayedUpdateOrientationWithWmLock(
400             @NonNull String cameraId, @NonNull String packageName) {
401         synchronized (this) {
402             if (!mScheduledOrientationUpdateCameraIdSet.remove(cameraId)) {
403                 // Orientation update has happened already or was cancelled because
404                 // camera was closed.
405                 return;
406             }
407             mCameraIdPackageBiMap.put(packageName, cameraId);
408         }
409         synchronized (mWmService.mGlobalLock) {
410             ActivityRecord topActivity = mDisplayContent.topRunningActivity(
411                         /* considerKeyguardState= */ true);
412             if (topActivity == null || topActivity.getTask() == null) {
413                 return;
414             }
415             // Checking whether an activity in fullscreen rather than the task as this camera
416             // compat treatment doesn't cover activity embedding.
417             if (topActivity.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
418                 topActivity.mLetterboxUiController.recomputeConfigurationForCameraCompatIfNeeded();
419                 mDisplayContent.updateOrientation();
420                 return;
421             }
422             // Checking that the whole app is in multi-window mode as we shouldn't show toast
423             // for the activity embedding case.
424             if (topActivity.getTask().getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW
425                     && isTreatmentEnabledForActivity(topActivity, /* mustBeFullscreen */ false)) {
426                 final PackageManager packageManager = mWmService.mContext.getPackageManager();
427                 try {
428                     showToast(
429                             R.string.display_rotation_camera_compat_toast_in_multi_window,
430                             (String) packageManager.getApplicationLabel(
431                                     packageManager.getApplicationInfo(packageName, /* flags */ 0)));
432                 } catch (PackageManager.NameNotFoundException e) {
433                     ProtoLog.e(WM_DEBUG_ORIENTATION,
434                             "DisplayRotationCompatPolicy: Multi-window toast not shown as "
435                                     + "package '%s' cannot be found.",
436                             packageName);
437                 }
438             }
439         }
440     }
441 
442     @VisibleForTesting
showToast(@tringRes int stringRes)443     void showToast(@StringRes int stringRes) {
444         UiThread.getHandler().post(
445                 () -> Toast.makeText(mWmService.mContext, stringRes, Toast.LENGTH_LONG).show());
446     }
447 
448     @VisibleForTesting
showToast(@tringRes int stringRes, @NonNull String applicationLabel)449     void showToast(@StringRes int stringRes, @NonNull String applicationLabel) {
450         UiThread.getHandler().post(
451                 () -> Toast.makeText(
452                         mWmService.mContext,
453                         mWmService.mContext.getString(stringRes, applicationLabel),
454                         Toast.LENGTH_LONG).show());
455     }
456 
notifyCameraClosed(@onNull String cameraId)457     private synchronized void notifyCameraClosed(@NonNull String cameraId) {
458         ProtoLog.v(WM_DEBUG_ORIENTATION,
459                 "Display id=%d is notified that Camera %s is closed, scheduling rotation update.",
460                 mDisplayContent.mDisplayId, cameraId);
461         mScheduledToBeRemovedCameraIdSet.add(cameraId);
462         // No need to update orientation for this camera if it's already closed.
463         mScheduledOrientationUpdateCameraIdSet.remove(cameraId);
464         scheduleRemoveCameraId(cameraId);
465     }
466 
467     // Delay is needed to avoid rotation flickering when an app is flipping between front and
468     // rear cameras, when size compat mode is restarted or activity is being refreshed.
scheduleRemoveCameraId(@onNull String cameraId)469     private void scheduleRemoveCameraId(@NonNull String cameraId) {
470         mHandler.postDelayed(
471                 () -> removeCameraId(cameraId),
472                 CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS);
473     }
474 
removeCameraId(String cameraId)475     private void removeCameraId(String cameraId) {
476         synchronized (this) {
477             if (!mScheduledToBeRemovedCameraIdSet.remove(cameraId)) {
478                 // Already reconnected to this camera, no need to clean up.
479                 return;
480             }
481             if (isActivityForCameraIdRefreshing(cameraId)) {
482                 ProtoLog.v(WM_DEBUG_ORIENTATION,
483                         "Display id=%d is notified that Camera %s is closed but activity is"
484                                 + " still refreshing. Rescheduling an update.",
485                         mDisplayContent.mDisplayId, cameraId);
486                 mScheduledToBeRemovedCameraIdSet.add(cameraId);
487                 scheduleRemoveCameraId(cameraId);
488                 return;
489             }
490             mCameraIdPackageBiMap.removeCameraId(cameraId);
491         }
492         ProtoLog.v(WM_DEBUG_ORIENTATION,
493                 "Display id=%d is notified that Camera %s is closed, updating rotation.",
494                 mDisplayContent.mDisplayId, cameraId);
495         synchronized (mWmService.mGlobalLock) {
496             ActivityRecord topActivity = mDisplayContent.topRunningActivity(
497                     /* considerKeyguardState= */ true);
498             if (topActivity == null
499                     // Checking whether an activity in fullscreen rather than the task as this
500                     // camera compat treatment doesn't cover activity embedding.
501                     || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
502                 return;
503             }
504             topActivity.mLetterboxUiController.recomputeConfigurationForCameraCompatIfNeeded();
505             mDisplayContent.updateOrientation();
506         }
507     }
508 
isActivityForCameraIdRefreshing(String cameraId)509     private boolean isActivityForCameraIdRefreshing(String cameraId) {
510         ActivityRecord topActivity = mDisplayContent.topRunningActivity(
511                 /* considerKeyguardState= */ true);
512         if (!isTreatmentEnabledForActivity(topActivity)) {
513             return false;
514         }
515         String activeCameraId = mCameraIdPackageBiMap.getCameraId(topActivity.packageName);
516         if (activeCameraId == null || activeCameraId != cameraId) {
517             return false;
518         }
519         return topActivity.mLetterboxUiController.isRefreshAfterRotationRequested();
520     }
521 
522     private static class CameraIdPackageNameBiMap {
523 
524         private final Map<String, String> mPackageToCameraIdMap = new ArrayMap<>();
525         private final Map<String, String> mCameraIdToPackageMap = new ArrayMap<>();
526 
isEmpty()527         boolean isEmpty() {
528             return mCameraIdToPackageMap.isEmpty();
529         }
530 
put(String packageName, String cameraId)531         void put(String packageName, String cameraId) {
532             // Always using the last connected camera ID for the package even for the concurrent
533             // camera use case since we can't guess which camera is more important anyway.
534             removePackageName(packageName);
535             removeCameraId(cameraId);
536             mPackageToCameraIdMap.put(packageName, cameraId);
537             mCameraIdToPackageMap.put(cameraId, packageName);
538         }
539 
containsPackageName(String packageName)540         boolean containsPackageName(String packageName) {
541             return mPackageToCameraIdMap.containsKey(packageName);
542         }
543 
544         @Nullable
getCameraId(String packageName)545         String getCameraId(String packageName) {
546             return mPackageToCameraIdMap.get(packageName);
547         }
548 
removeCameraId(String cameraId)549         void removeCameraId(String cameraId) {
550             String packageName = mCameraIdToPackageMap.get(cameraId);
551             if (packageName == null) {
552                 return;
553             }
554             mPackageToCameraIdMap.remove(packageName, cameraId);
555             mCameraIdToPackageMap.remove(cameraId, packageName);
556         }
557 
getSummaryForDisplayRotationHistoryRecord()558         String getSummaryForDisplayRotationHistoryRecord() {
559             return "{ mPackageToCameraIdMap=" + mPackageToCameraIdMap + " }";
560         }
561 
removePackageName(String packageName)562         private void removePackageName(String packageName) {
563             String cameraId = mPackageToCameraIdMap.get(packageName);
564             if (cameraId == null) {
565                 return;
566             }
567             mPackageToCameraIdMap.remove(packageName, cameraId);
568             mCameraIdToPackageMap.remove(cameraId, packageName);
569         }
570     }
571 }
572