diff options
author | Mariia Sandrikova <mariiasand@google.com> | 2021-12-15 02:34:05 +0000 |
---|---|---|
committer | Mariia Sandrikova <mariiasand@google.com> | 2021-12-17 00:18:57 +0000 |
commit | 24f4d8a9521d0281721fe5703b32569c8fd7e151 (patch) | |
tree | 6eb5c0f8575a289e19cdc1447fed3958704f3f73 | |
parent | aa7a8cdda5c7bf7ad30c93bdec3dd875ca6985e5 (diff) |
[2/n] Camera Compat UI: Add interfaces for client-server communication.
Changes:
- Listens to changes from the client coming through IActivityClientController#requestCompatCameraControl to ActivityRecord#updateCameraCompatState
- ActivityRecord#updateCameraCompatState sends updated state via TaskInfo to WM Shell
- ITaskOrganizerController#updateCameraCompatControlState to dispatch the user interactions with the control from WM Shell triggers callback to ActivityRecord#updateCameraCompatStateFromUser
- ActivityRecord#updateCameraCompatStateFromUser remembers the user's choice and asks client to apply treatment through ICompatCameraControlCallback
Feature is guarded with config_isCameraCompatControlForStretchedIssuesEnabled
Test: atest WMShellUnitTests:ShellTaskOrganizerTests, atest WmTests:ActivityRecordTests
Bug: 206602997
Change-Id: I083aa6718bd67456bedd9444e9b78740c041f870
20 files changed, 705 insertions, 39 deletions
diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java index db7ab1a6f379..eb4a355c8ae7 100644 --- a/core/java/android/app/ActivityClient.java +++ b/core/java/android/app/ActivityClient.java @@ -20,6 +20,7 @@ import android.annotation.Nullable; import android.content.ComponentName; import android.content.Intent; import android.content.res.Configuration; +import android.content.res.Resources; import android.os.Bundle; import android.os.IBinder; import android.os.PersistableBundle; @@ -498,6 +499,28 @@ public class ActivityClient { } } + /** + * Shows or hides a Camera app compat toggle for stretched issues with the requested state. + * + * @param token The token for the window that needs a control. + * @param showControl Whether the control should be shown or hidden. + * @param transformationApplied Whether the treatment is already applied. + * @param callback The callback executed when the user clicks on a control. + */ + void requestCompatCameraControl(Resources res, IBinder token, boolean showControl, + boolean transformationApplied, ICompatCameraControlCallback callback) { + if (!res.getBoolean(com.android.internal.R.bool + .config_isCameraCompatControlForStretchedIssuesEnabled)) { + return; + } + try { + getActivityClientController().requestCompatCameraControl( + token, showControl, transformationApplied, callback); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + public static ActivityClient getInstance() { return sInstance.get(); } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 76e392d6cead..a042430268b2 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -561,8 +561,8 @@ public final class ActivityThread extends ClientTransactionHandler private Configuration mPendingOverrideConfig; // Used for consolidating configs before sending on to Activity. private Configuration tmpConfig = new Configuration(); - // Callback used for updating activity override config. - ViewRootImpl.ActivityConfigCallback configCallback; + // Callback used for updating activity override config and camera compat control state. + ViewRootImpl.ActivityConfigCallback activityConfigCallback; ActivityClientRecord nextIdle; // Indicates whether this activity is currently the topmost resumed one in the system. @@ -660,13 +660,30 @@ public final class ActivityThread extends ClientTransactionHandler stopped = false; hideForNow = false; nextIdle = null; - configCallback = (Configuration overrideConfig, int newDisplayId) -> { - if (activity == null) { - throw new IllegalStateException( - "Received config update for non-existing activity"); + activityConfigCallback = new ViewRootImpl.ActivityConfigCallback() { + @Override + public void onConfigurationChanged(Configuration overrideConfig, + int newDisplayId) { + if (activity == null) { + throw new IllegalStateException( + "Received config update for non-existing activity"); + } + activity.mMainThread.handleActivityConfigurationChanged( + ActivityClientRecord.this, overrideConfig, newDisplayId); } - activity.mMainThread.handleActivityConfigurationChanged(this, overrideConfig, - newDisplayId); + + @Override + public void requestCompatCameraControl(boolean showControl, + boolean transformationApplied, ICompatCameraControlCallback callback) { + if (activity == null) { + throw new IllegalStateException( + "Received camera compat control update for non-existing activity"); + } + ActivityClient.getInstance().requestCompatCameraControl( + activity.getResources(), token, showControl, transformationApplied, + callback); + } + }; } @@ -3647,7 +3664,7 @@ public final class ActivityThread extends ClientTransactionHandler activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID, r.lastNonConfigurationInstances, config, - r.referrer, r.voiceInteractor, window, r.configCallback, + r.referrer, r.voiceInteractor, window, r.activityConfigCallback, r.assistToken, r.shareableActivityToken); if (customIntent != null) { @@ -5489,8 +5506,8 @@ public final class ActivityThread extends ClientTransactionHandler } else { final ViewRootImpl viewRoot = v.getViewRootImpl(); if (viewRoot != null) { - // Clear the callback to avoid the destroyed activity from receiving - // configuration changes that are no longer effective. + // Clear callbacks to avoid the destroyed activity from receiving + // configuration or camera compat changes that are no longer effective. viewRoot.setActivityConfigCallback(null); } wm.removeViewImmediate(v); diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl index aba6eb9229f2..83c57c573b82 100644 --- a/core/java/android/app/IActivityClientController.aidl +++ b/core/java/android/app/IActivityClientController.aidl @@ -17,6 +17,7 @@ package android.app; import android.app.ActivityManager; +import android.app.ICompatCameraControlCallback; import android.app.IRequestFinishCallback; import android.app.PictureInPictureParams; import android.content.ComponentName; @@ -143,4 +144,15 @@ interface IActivityClientController { /** Reports that the splash screen view has attached to activity. */ oneway void splashScreenAttached(in IBinder token); + + /** + * Shows or hides a Camera app compat toggle for stretched issues with the requested state. + * + * @param token The token for the window that needs a control. + * @param showControl Whether the control should be shown or hidden. + * @param transformationApplied Whether the treatment is already applied. + * @param callback The callback executed when the user clicks on a control. + */ + oneway void requestCompatCameraControl(in IBinder token, boolean showControl, + boolean transformationApplied, in ICompatCameraControlCallback callback); } diff --git a/core/java/android/app/ICompatCameraControlCallback.aidl b/core/java/android/app/ICompatCameraControlCallback.aidl new file mode 100644 index 000000000000..1a7f21066630 --- /dev/null +++ b/core/java/android/app/ICompatCameraControlCallback.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2021 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 android.app; + +/** + * This callback allows ActivityRecord to ask the calling View to apply the treatment for stretched + * issues affecting camera viewfinders when the user clicks on the camera compat control. + * + * {@hide} + */ +oneway interface ICompatCameraControlCallback { + + void applyCameraCompatTreatment(); + + void revertCameraCompatTreatment(); +} diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index fd6fa57b9e8d..9f4107c30b7c 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -1259,7 +1259,7 @@ public class Instrumentation { info, title, parent, id, (Activity.NonConfigurationInstances)lastNonConfigurationInstance, new Configuration(), null /* referrer */, null /* voiceInteractor */, - null /* window */, null /* activityConfigCallback */, null /*assistToken*/, + null /* window */, null /* activityCallback */, null /*assistToken*/, null /*shareableActivityToken*/); return activity; } diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java index ddde27220b96..cd885c1b13ca 100644 --- a/core/java/android/app/TaskInfo.java +++ b/core/java/android/app/TaskInfo.java @@ -19,6 +19,7 @@ package android.app; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; @@ -39,6 +40,8 @@ import android.view.DisplayCutout; import android.window.TaskSnapshot; import android.window.WindowContainerToken; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Objects; @@ -256,6 +259,51 @@ public class TaskInfo { */ public boolean isSleeping; + /** + * Camera compat control isn't shown because it's not requested by heuristics. + * @hide + */ + public static final int CAMERA_COMPAT_CONTROL_HIDDEN = 0; + + /** + * Camera compat control is shown with the treatment suggested. + * @hide + */ + public static final int CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED = 1; + + /** + * Camera compat control is shown to allow reverting the applied treatment. + * @hide + */ + public static final int CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED = 2; + + /** + * Camera compat control is dismissed by user. + * @hide + */ + public static final int CAMERA_COMPAT_CONTROL_DISMISSED = 3; + + /** + * Enum for the Camera app compat control states. + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "CAMERA_COMPAT_CONTROL_" }, value = { + CAMERA_COMPAT_CONTROL_HIDDEN, + CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED, + CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED, + CAMERA_COMPAT_CONTROL_DISMISSED, + }) + public @interface CameraCompatControlState {}; + + /** + * State of the Camera app compat control which is used to correct stretched viewfinder + * in apps that don't handle all possible configurations and changes between them correctly. + * @hide + */ + @CameraCompatControlState + public int cameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN; + TaskInfo() { // Do nothing } @@ -324,6 +372,17 @@ public class TaskInfo { launchCookies.add(cookie); } + /** @hide */ + public boolean hasCameraCompatControl() { + return cameraCompatControlState != CAMERA_COMPAT_CONTROL_HIDDEN + && cameraCompatControlState != CAMERA_COMPAT_CONTROL_DISMISSED; + } + + /** @hide */ + public boolean hasCompatUI() { + return hasCameraCompatControl() || topActivityInSizeCompat; + } + /** * @return {@code true} if this task contains the launch cookie. * @hide @@ -376,19 +435,20 @@ public class TaskInfo { * @return {@code true} if parameters that are important for size compat have changed. * @hide */ - public boolean equalsForSizeCompat(@Nullable TaskInfo that) { + public boolean equalsForCompatUi(@Nullable TaskInfo that) { if (that == null) { return false; } return displayId == that.displayId && taskId == that.taskId && topActivityInSizeCompat == that.topActivityInSizeCompat - // Bounds are important if top activity is in size compat - && (!topActivityInSizeCompat || configuration.windowConfiguration.getBounds() + && cameraCompatControlState == that.cameraCompatControlState + // Bounds are important if top activity has compat controls. + && (!hasCompatUI() || configuration.windowConfiguration.getBounds() .equals(that.configuration.windowConfiguration.getBounds())) - && (!topActivityInSizeCompat || configuration.getLayoutDirection() + && (!hasCompatUI() || configuration.getLayoutDirection() == that.configuration.getLayoutDirection()) - && (!topActivityInSizeCompat || isVisible == that.isVisible); + && (!hasCompatUI() || isVisible == that.isVisible); } /** @@ -428,6 +488,7 @@ public class TaskInfo { topActivityInSizeCompat = source.readBoolean(); mTopActivityLocusId = source.readTypedObject(LocusId.CREATOR); displayAreaFeatureId = source.readInt(); + cameraCompatControlState = source.readInt(); } /** @@ -468,6 +529,7 @@ public class TaskInfo { dest.writeBoolean(topActivityInSizeCompat); dest.writeTypedObject(mTopActivityLocusId, flags); dest.writeInt(displayAreaFeatureId); + dest.writeInt(cameraCompatControlState); } @Override @@ -498,6 +560,22 @@ public class TaskInfo { + " topActivityInSizeCompat=" + topActivityInSizeCompat + " locusId=" + mTopActivityLocusId + " displayAreaFeatureId=" + displayAreaFeatureId + + " cameraCompatControlState=" + + cameraCompatControlStateToString(cameraCompatControlState) + "}"; } + + /** @hide */ + public static String cameraCompatControlStateToString( + @CameraCompatControlState int cameraCompatControlState) { + switch (cameraCompatControlState) { + case CAMERA_COMPAT_CONTROL_HIDDEN: return "hidden"; + case CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED: return "treatment-suggested"; + case CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED: return "treatment-applied"; + case CAMERA_COMPAT_CONTROL_DISMISSED: return "dismissed"; + default: + throw new AssertionError( + "Unexpected camera compat control state: " + cameraCompatControlState); + } + } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index f1eb783726db..74b97312b805 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -89,6 +89,7 @@ import android.annotation.Nullable; import android.annotation.UiContext; import android.app.ActivityManager; import android.app.ActivityThread; +import android.app.ICompatCameraControlCallback; import android.app.ResourcesManager; import android.compat.annotation.UnsupportedAppUsage; import android.content.ClipData; @@ -329,7 +330,7 @@ public final class ViewRootImpl implements ViewParent, private static final ArrayList<ConfigChangedCallback> sConfigCallbacks = new ArrayList<>(); /** - * Callback for notifying activities about override configuration changes. + * Callback for notifying activities. */ public interface ActivityConfigCallback { @@ -339,11 +340,23 @@ public final class ViewRootImpl implements ViewParent, * @param newDisplayId New display id, {@link Display#INVALID_DISPLAY} if not changed. */ void onConfigurationChanged(Configuration overrideConfig, int newDisplayId); + + /** + * Notify the corresponding activity about the request to show or hide a camera compat + * control for stretched issues in the viewfinder. + * + * @param showControl Whether the control should be shown or hidden. + * @param transformationApplied Whether the treatment is already applied. + * @param callback The callback executed when the user clicks on a control. + */ + void requestCompatCameraControl(boolean showControl, boolean transformationApplied, + ICompatCameraControlCallback callback); } /** - * Callback used to notify corresponding activity about override configuration change and make - * sure that all resources are set correctly before updating the ViewRootImpl's internal state. + * Callback used to notify corresponding activity about camera compat control changes, override + * configuration change and make sure that all resources are set correctly before updating the + * ViewRootImpl's internal state. */ private ActivityConfigCallback mActivityConfigCallback; @@ -895,7 +908,10 @@ public final class ViewRootImpl implements ViewParent, } } - /** Add activity config callback to be notified about override config changes. */ + /** + * Add activity config callback to be notified about override config changes and camera + * compat control state updates. + */ public void setActivityConfigCallback(ActivityConfigCallback callback) { mActivityConfigCallback = callback; } @@ -10600,6 +10616,20 @@ public final class ViewRootImpl implements ViewParent, } /** + * Shows or hides a Camera app compat toggle for stretched issues with the requested state + * for the corresponding activity. + * + * @param showControl Whether the control should be shown or hidden. + * @param transformationApplied Whether the treatment is already applied. + * @param callback The callback executed when the user clicks on a control. + */ + public void requestCompatCameraControl(boolean showControl, boolean transformationApplied, + ICompatCameraControlCallback callback) { + mActivityConfigCallback.requestCompatCameraControl( + showControl, transformationApplied, callback); + } + + /** * Redirect the next draw of this ViewRoot (from the UI thread perspective) * to the passed in consumer. This can be used to create P2P synchronization * between ViewRoot's however it comes with many caveats. diff --git a/core/java/android/window/ITaskOrganizerController.aidl b/core/java/android/window/ITaskOrganizerController.aidl index a833600e1fbc..022d05da8825 100644 --- a/core/java/android/window/ITaskOrganizerController.aidl +++ b/core/java/android/window/ITaskOrganizerController.aidl @@ -66,4 +66,7 @@ interface ITaskOrganizerController { * Restarts the top activity in the given task by killing its process if it is visible. */ void restartTaskTopActivityProcessIfVisible(in WindowContainerToken task); + + /** Updates a state of camera compat control for stretched issues in the viewfinder. */ + void updateCameraCompatControlState(in WindowContainerToken task, int state); } diff --git a/core/java/android/window/TaskOrganizer.java b/core/java/android/window/TaskOrganizer.java index 27c7d3158f95..3ec18dbe0ebc 100644 --- a/core/java/android/window/TaskOrganizer.java +++ b/core/java/android/window/TaskOrganizer.java @@ -24,6 +24,7 @@ import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.TestApi; import android.app.ActivityManager; +import android.app.TaskInfo.CameraCompatControlState; import android.os.IBinder; import android.os.RemoteException; import android.view.SurfaceControl; @@ -238,6 +239,20 @@ public class TaskOrganizer extends WindowOrganizer { } /** + * Updates a state of camera compat control for stretched issues in the viewfinder. + * @hide + */ + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) + public void updateCameraCompatControlState(@NonNull WindowContainerToken task, + @CameraCompatControlState int state) { + try { + mTaskOrganizerController.updateCameraCompatControlState(task, state); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Gets the executor to run callbacks on. * @hide */ diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 1c31b1b76f85..7bedcc67dc6d 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4938,6 +4938,10 @@ If given value is outside of this range, the option 1 (center) is assummed. --> <integer name="config_letterboxDefaultPositionForReachability">1</integer> + <!-- Whether a camera compat controller is enabled to allow the user to apply or revert + treatment for stretched issues in camera viewfinder. --> + <bool name="config_isCameraCompatControlForStretchedIssuesEnabled">false</bool> + <!-- If true, hide the display cutout with display area --> <bool name="config_hideDisplayCutoutWithDisplayArea">false</bool> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index b017a30cb5f2..46b249e5be6d 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4273,6 +4273,7 @@ <java-symbol type="dimen" name="config_letterboxHorizontalPositionMultiplier" /> <java-symbol type="bool" name="config_letterboxIsReachabilityEnabled" /> <java-symbol type="integer" name="config_letterboxDefaultPositionForReachability" /> + <java-symbol type="bool" name="config_isCameraCompatControlForStretchedIssuesEnabled" /> <java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" /> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 6bfbd8d55ab0..9584994774a9 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -1201,6 +1201,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-846931068": { + "message": "Update camera compat control state to %s for taskId=%d", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_ORGANIZER", + "at": "com\/android\/server\/wm\/TaskOrganizerController.java" + }, "-846078709": { "message": "Configuration doesn't matter in finishing %s", "level": "VERBOSE", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 8b3a35688f11..1bbc9a508233 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -458,7 +458,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements newListener.onTaskInfoChanged(taskInfo); } notifyLocusVisibilityIfNeeded(taskInfo); - if (updated || !taskInfo.equalsForSizeCompat(data.getTaskInfo())) { + if (updated || !taskInfo.equalsForCompatUi(data.getTaskInfo())) { // Notify the compat UI if the listener or task info changed. notifyCompatUI(taskInfo, newListener); } @@ -633,7 +633,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements // The task is vanished or doesn't support compat UI, notify to remove compat UI // on this Task if there is any. if (taskListener == null || !taskListener.supportCompatUI() - || !taskInfo.topActivityInSizeCompat || !taskInfo.isVisible) { + || !taskInfo.hasCompatUI() || !taskInfo.isVisible) { mCompatUI.onCompatInfoChanged(taskInfo.displayId, taskInfo.taskId, null /* taskConfig */, null /* taskListener */); return; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index a3b98a8fc880..7c204e636588 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -37,6 +37,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import android.app.ActivityManager.RunningTaskInfo; +import android.app.TaskInfo; import android.content.Context; import android.content.LocusId; import android.content.pm.ParceledListSlice; @@ -366,6 +367,84 @@ public class ShellTaskOrganizerTests { } @Test + public void testOnCameraCompatActivityChanged() { + final RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN); + taskInfo1.displayId = DEFAULT_DISPLAY; + taskInfo1.cameraCompatControlState = TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; + final TrackingTaskListener taskListener = new TrackingTaskListener(); + mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN); + mOrganizer.onTaskAppeared(taskInfo1, null); + + // Task listener sent to compat UI is null if top activity doesn't request a camera + // compat control. + verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + null /* taskConfig */, null /* taskListener */); + + // Task linster is non-null when request a camera compat control for a visible task. + clearInvocations(mCompatUI); + final RunningTaskInfo taskInfo2 = + createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); + taskInfo2.displayId = taskInfo1.displayId; + taskInfo2.cameraCompatControlState = TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; + taskInfo2.isVisible = true; + mOrganizer.onTaskInfoChanged(taskInfo2); + verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + taskInfo1.configuration, taskListener); + + // CompatUIController#onCompatInfoChanged is called when requested state for a camera + // compat control changes for a visible task. + clearInvocations(mCompatUI); + final RunningTaskInfo taskInfo3 = + createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); + taskInfo3.displayId = taskInfo1.displayId; + taskInfo3.cameraCompatControlState = TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; + taskInfo3.isVisible = true; + mOrganizer.onTaskInfoChanged(taskInfo3); + verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + taskInfo1.configuration, taskListener); + + // CompatUIController#onCompatInfoChanged is called when a top activity goes in size compat + // mode for a visible task that has a compat control. + clearInvocations(mCompatUI); + final RunningTaskInfo taskInfo4 = + createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); + taskInfo4.displayId = taskInfo1.displayId; + taskInfo4.topActivityInSizeCompat = true; + taskInfo4.cameraCompatControlState = TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; + taskInfo4.isVisible = true; + mOrganizer.onTaskInfoChanged(taskInfo4); + verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + taskInfo1.configuration, taskListener); + + // Task linster is null when a camera compat control is dimissed for a visible task. + clearInvocations(mCompatUI); + final RunningTaskInfo taskInfo5 = + createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); + taskInfo5.displayId = taskInfo1.displayId; + taskInfo5.cameraCompatControlState = TaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; + taskInfo5.isVisible = true; + mOrganizer.onTaskInfoChanged(taskInfo5); + verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + null /* taskConfig */, null /* taskListener */); + + // Task linster is null when request a camera compat control for a invisible task. + clearInvocations(mCompatUI); + final RunningTaskInfo taskInfo6 = + createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); + taskInfo6.displayId = taskInfo1.displayId; + taskInfo6.cameraCompatControlState = TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; + taskInfo6.isVisible = false; + mOrganizer.onTaskInfoChanged(taskInfo6); + verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + null /* taskConfig */, null /* taskListener */); + + clearInvocations(mCompatUI); + mOrganizer.onTaskVanished(taskInfo1); + verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + null /* taskConfig */, null /* taskListener */); + } + + @Test public void testAddLocusListener() { RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); task1.isVisible = true; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 44b45401ad77..e36857769a6d 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -39,11 +39,13 @@ import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ExitTransitionCoordinator; import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks; +import android.app.ICompatCameraControlCallback; import android.app.Notification; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Insets; import android.graphics.PixelFormat; @@ -72,6 +74,7 @@ import android.view.RemoteAnimationTarget; import android.view.ScrollCaptureResponse; import android.view.SurfaceControl; import android.view.View; +import android.view.ViewRootImpl; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowInsets; @@ -597,20 +600,35 @@ public class ScreenshotController { withWindowAttached(() -> { requestScrollCapture(); mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback( - (overrideConfig, newDisplayId) -> { - if (mConfigChanges.applyNewConfig(mContext.getResources())) { - // Hide the scroll chip until we know it's available in this orientation - mScreenshotView.hideScrollChip(); - // Delay scroll capture eval a bit to allow the underlying activity - // to set up in the new orientation. - mScreenshotHandler.postDelayed(this::requestScrollCapture, 150); - mScreenshotView.updateInsets( - mWindowManager.getCurrentWindowMetrics().getWindowInsets()); - // screenshot animation calculations won't be valid anymore, so just end - if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { - mScreenshotAnimation.end(); + new ViewRootImpl.ActivityConfigCallback() { + @Override + public void onConfigurationChanged(Configuration overrideConfig, + int newDisplayId) { + if (mConfigChanges.applyNewConfig(mContext.getResources())) { + // Hide the scroll chip until we know it's available in this + // orientation + mScreenshotView.hideScrollChip(); + // Delay scroll capture eval a bit to allow the underlying activity + // to set up in the new orientation. + mScreenshotHandler.postDelayed( + ScreenshotController.this::requestScrollCapture, 150); + mScreenshotView.updateInsets( + mWindowManager.getCurrentWindowMetrics() + .getWindowInsets()); + // Screenshot animation calculations won't be valid anymore, + // so just end + if (mScreenshotAnimation != null + && mScreenshotAnimation.isRunning()) { + mScreenshotAnimation.end(); + } } } + @Override + public void requestCompatCameraControl(boolean showControl, + boolean transformationApplied, + ICompatCameraControlCallback callback) { + Log.w(TAG, "Unexpected requestCompatCameraControl callback"); + } }); }); diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index ee72fc8622a5..e9ffcc07fca8 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -49,6 +49,7 @@ import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.IActivityClientController; +import android.app.ICompatCameraControlCallback; import android.app.IRequestFinishCallback; import android.app.PictureInPictureParams; import android.app.PictureInPictureUiState; @@ -766,6 +767,22 @@ class ActivityClientController extends IActivityClientController.Stub { Binder.restoreCallingIdentity(origId); } + @Override + public void requestCompatCameraControl(IBinder token, boolean showControl, + boolean transformationApplied, ICompatCameraControlCallback callback) { + final long origId = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token); + if (r != null) { + r.updateCameraCompatState(showControl, transformationApplied, callback); + } + } + } finally { + Binder.restoreCallingIdentity(origId); + } + } + /** * Checks the state of the system and the activity associated with the given {@param token} to * verify that picture-in-picture is supported for that activity. diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 393d101e9830..f841fc7692f1 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -235,9 +235,12 @@ import android.annotation.Size; import android.app.Activity; import android.app.ActivityManager.TaskDescription; import android.app.ActivityOptions; +import android.app.ICompatCameraControlCallback; import android.app.PendingIntent; import android.app.PictureInPictureParams; import android.app.ResultInfo; +import android.app.TaskInfo; +import android.app.TaskInfo.CameraCompatControlState; import android.app.WaitResult; import android.app.WindowConfiguration; import android.app.servertransaction.ActivityConfigurationChangeItem; @@ -723,6 +726,20 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Nullable private Rect mLetterboxBoundsForFixedOrientationAndAspectRatio; + // State of the Camera app compat control which is used to correct stretched viewfinder + // in apps that don't handle all possible configurations and changes between them correctly. + @CameraCompatControlState + private int mCameraCompatControlState = TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; + + + // The callback that allows to ask the calling View to apply the treatment for stretched + // issues affecting camera viewfinders when the user clicks on the camera compat control. + @Nullable + private ICompatCameraControlCallback mCompatCameraControlCallback; + + private final boolean mCameraCompatControlEnabled; + private boolean mCameraCompatControlClickedByUser; + // activity is not displayed? // TODO: rename to mNoDisplay @VisibleForTesting @@ -1176,6 +1193,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } mLetterboxUiController.dump(pw, prefix); + + pw.println(prefix + "mCameraCompatControlState=" + + TaskInfo.cameraCompatControlStateToString(mCameraCompatControlState)); + pw.println(prefix + "mCameraCompatControlEnabled=" + mCameraCompatControlEnabled); } static boolean dumpActivity(FileDescriptor fd, PrintWriter pw, int index, ActivityRecord r, @@ -1575,6 +1596,91 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mLetterboxUiController.getLetterboxInnerBounds(outBounds); } + void updateCameraCompatState(boolean showControl, boolean transformationApplied, + ICompatCameraControlCallback callback) { + if (!isCameraCompatControlEnabled()) { + // Feature is disabled by config_isCameraCompatControlForStretchedIssuesEnabled. + return; + } + if (mCameraCompatControlClickedByUser && (showControl + || mCameraCompatControlState == TaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED)) { + // The user already applied treatment on this activity or dismissed control. + // Respecting their choice. + return; + } + mCompatCameraControlCallback = callback; + int newCameraCompatControlState = !showControl + ? TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN + : transformationApplied + ? TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED + : TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; + boolean changed = setCameraCompatControlState(newCameraCompatControlState); + if (!changed) { + return; + } + if (newCameraCompatControlState == TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN) { + mCameraCompatControlClickedByUser = false; + mCompatCameraControlCallback = null; + } + // Trigger TaskInfoChanged to update the camera compat UI. + getTask().dispatchTaskInfoChangedIfNeeded(true /* force */); + } + + void updateCameraCompatStateFromUser(@CameraCompatControlState int state) { + if (!isCameraCompatControlEnabled()) { + // Feature is disabled by config_isCameraCompatControlForStretchedIssuesEnabled. + return; + } + if (state == TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN) { + Slog.w(TAG, "Unexpected hidden state in updateCameraCompatState"); + return; + } + boolean changed = setCameraCompatControlState(state); + mCameraCompatControlClickedByUser = true; + if (!changed) { + return; + } + if (state == TaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED) { + mCompatCameraControlCallback = null; + return; + } + if (mCompatCameraControlCallback == null) { + Slog.w(TAG, "Callback for a camera compat control is null"); + return; + } + try { + if (state == TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED) { + mCompatCameraControlCallback.applyCameraCompatTreatment(); + } else { + mCompatCameraControlCallback.revertCameraCompatTreatment(); + } + } catch (RemoteException e) { + Slog.e(TAG, "Unable to apply or revert camera compat treatment", e); + } + } + + private boolean setCameraCompatControlState(@CameraCompatControlState int state) { + if (!isCameraCompatControlEnabled()) { + // Feature is disabled by config_isCameraCompatControlForStretchedIssuesEnabled. + return false; + } + if (mCameraCompatControlState != state) { + mCameraCompatControlState = state; + return true; + } + return false; + } + + @CameraCompatControlState + int getCameraCompatControlState() { + return mCameraCompatControlState; + } + + @VisibleForTesting + boolean isCameraCompatControlEnabled() { + return mCameraCompatControlEnabled; + } + /** * @return {@code true} if bar shown within a given rectangle is allowed to be fully transparent * when the current activity is displayed. @@ -1836,6 +1942,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A taskDescription = _taskDescription; mLetterboxUiController = new LetterboxUiController(mWmService, this); + mCameraCompatControlEnabled = mWmService.mContext.getResources() + .getBoolean(R.bool.config_isCameraCompatControlForStretchedIssuesEnabled); if (_createTime > 0) { createTime = _createTime; diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index c88dbf719d94..a8032f4d2954 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3460,11 +3460,18 @@ class Task extends TaskFragment { info.topActivityInfo = mReuseActivitiesReport.top != null ? mReuseActivitiesReport.top.info : null; + + boolean isTopActivityResumed = mReuseActivitiesReport.top != null + && mReuseActivitiesReport.top.getOrganizedTask() == this + && mReuseActivitiesReport.top.isState(RESUMED); // Whether the direct top activity is in size compat mode on foreground. - info.topActivityInSizeCompat = mReuseActivitiesReport.top != null - && mReuseActivitiesReport.top.getOrganizedTask() == this - && mReuseActivitiesReport.top.inSizeCompatMode() - && mReuseActivitiesReport.top.isState(RESUMED); + info.topActivityInSizeCompat = isTopActivityResumed + && mReuseActivitiesReport.top.inSizeCompatMode(); + // Whether the direct top activity requested showing camera compat control. + info.cameraCompatControlState = isTopActivityResumed + ? mReuseActivitiesReport.top.getCameraCompatControlState() + : TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; + info.launchCookies.clear(); info.addLaunchCookie(mLaunchCookie); forAllActivities(r -> { diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java index 3d5f9881e044..037d582edc30 100644 --- a/services/core/java/com/android/server/wm/TaskOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java @@ -16,6 +16,8 @@ package com.android.server.wm; +import static android.app.TaskInfo.cameraCompatControlStateToString; + import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER; import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission; import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING; @@ -931,6 +933,35 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { } } + @Override + public void updateCameraCompatControlState(WindowContainerToken token, int state) { + enforceTaskPermission("updateCameraCompatControlState()"); + final long origId = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + final WindowContainer wc = WindowContainer.fromBinder(token.asBinder()); + if (wc == null) { + Slog.w(TAG, "Could not resolve window from token"); + return; + } + final Task task = wc.asTask(); + if (task == null) { + Slog.w(TAG, "Could not resolve task from token"); + return; + } + ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, + "Update camera compat control state to %s for taskId=%d", + cameraCompatControlStateToString(state), task.mTaskId); + final ActivityRecord activity = task.getTopNonFinishingActivity(); + if (activity != null) { + activity.updateCameraCompatStateFromUser(state); + } + } + } finally { + Binder.restoreCallingIdentity(origId); + } + } + public boolean handleInterceptBackPressedOnTaskRoot(Task task) { if (task == null || !task.isOrganized() || !mInterceptBackPressedOnRootTasks.contains(task.mTaskId)) { diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 9a68b5f1b609..a9a603b62d26 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -16,6 +16,10 @@ package com.android.server.wm; +import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; +import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; +import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; +import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; @@ -103,6 +107,7 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; import android.app.ActivityOptions; +import android.app.ICompatCameraControlCallback; import android.app.servertransaction.ActivityConfigurationChangeItem; import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.DestroyActivityItem; @@ -3098,6 +3103,188 @@ public class ActivityRecordTests extends WindowTestsBase { eq(null)); } + @Test + public void testUpdateCameraCompatState_flagIsEnabled_controlStateIsUpdated() { + final ActivityRecord activity = createActivityWithTask(); + // Mock a flag being enabled. + doReturn(true).when(activity).isCameraCompatControlEnabled(); + + activity.updateCameraCompatState(/* showControl */ true, + /* transformationApplied */ false, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), + CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + + activity.updateCameraCompatState(/* showControl */ true, + /* transformationApplied */ true, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), + CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); + + activity.updateCameraCompatState(/* showControl */ false, + /* transformationApplied */ false, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_HIDDEN); + + activity.updateCameraCompatState(/* showControl */ false, + /* transformationApplied */ true, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_HIDDEN); + } + + @Test + public void testUpdateCameraCompatState_flagIsDisabled_controlStateIsHidden() { + final ActivityRecord activity = createActivityWithTask(); + // Mock a flag being disabled. + doReturn(false).when(activity).isCameraCompatControlEnabled(); + + activity.updateCameraCompatState(/* showControl */ true, + /* transformationApplied */ false, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_HIDDEN); + + activity.updateCameraCompatState(/* showControl */ true, + /* transformationApplied */ true, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_HIDDEN); + } + + @Test + public void testUpdateCameraCompatStateFromUser_clickedOnDismiss() throws RemoteException { + final ActivityRecord activity = createActivityWithTask(); + // Mock a flag being enabled. + doReturn(true).when(activity).isCameraCompatControlEnabled(); + + ICompatCameraControlCallback callback = getCompatCameraControlCallback(); + spyOn(callback); + activity.updateCameraCompatState(/* showControl */ true, + /* transformationApplied */ false, callback); + + assertEquals(activity.getCameraCompatControlState(), + CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + + // Clicking on the button. + activity.updateCameraCompatStateFromUser(CAMERA_COMPAT_CONTROL_DISMISSED); + + verify(callback, never()).revertCameraCompatTreatment(); + verify(callback, never()).applyCameraCompatTreatment(); + assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_DISMISSED); + + // All following updates are ignored. + activity.updateCameraCompatState(/* showControl */ true, + /* transformationApplied */ false, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_DISMISSED); + + activity.updateCameraCompatState(/* showControl */ true, + /* transformationApplied */ true, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_DISMISSED); + + activity.updateCameraCompatState(/* showControl */ false, + /* transformationApplied */ true, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_DISMISSED); + } + + @Test + public void testUpdateCameraCompatStateFromUser_clickedOnApplyTreatment() + throws RemoteException { + final ActivityRecord activity = createActivityWithTask(); + // Mock a flag being enabled. + doReturn(true).when(activity).isCameraCompatControlEnabled(); + + ICompatCameraControlCallback callback = getCompatCameraControlCallback(); + spyOn(callback); + activity.updateCameraCompatState(/* showControl */ true, + /* transformationApplied */ false, callback); + + assertEquals(activity.getCameraCompatControlState(), + CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + + // Clicking on the button. + activity.updateCameraCompatStateFromUser(CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); + + verify(callback, never()).revertCameraCompatTreatment(); + verify(callback).applyCameraCompatTreatment(); + assertEquals(activity.getCameraCompatControlState(), + CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); + + // Request from the client to show the control are ignored respecting the user choice. + activity.updateCameraCompatState(/* showControl */ true, + /* transformationApplied */ false, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), + CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); + + // Request from the client to hide the control is respected. + activity.updateCameraCompatState(/* showControl */ false, + /* transformationApplied */ true, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_HIDDEN); + + // Request from the client to show the control again is respected. + activity.updateCameraCompatState(/* showControl */ true, + /* transformationApplied */ false, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), + CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + } + + @Test + public void testUpdateCameraCompatStateFromUser_clickedOnRevertTreatment() + throws RemoteException { + final ActivityRecord activity = createActivityWithTask(); + // Mock a flag being enabled. + doReturn(true).when(activity).isCameraCompatControlEnabled(); + + ICompatCameraControlCallback callback = getCompatCameraControlCallback(); + spyOn(callback); + activity.updateCameraCompatState(/* showControl */ true, + /* transformationApplied */ true, callback); + + assertEquals(activity.getCameraCompatControlState(), + CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); + + // Clicking on the button. + activity.updateCameraCompatStateFromUser(CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + + verify(callback).revertCameraCompatTreatment(); + verify(callback, never()).applyCameraCompatTreatment(); + assertEquals(activity.getCameraCompatControlState(), + CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + + // Request from the client to show the control are ignored respecting the user choice. + activity.updateCameraCompatState(/* showControl */ true, + /* transformationApplied */ true, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), + CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + + // Request from the client to hide the control is respected. + activity.updateCameraCompatState(/* showControl */ false, + /* transformationApplied */ true, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_HIDDEN); + + // Request from the client to show the control again is respected. + activity.updateCameraCompatState(/* showControl */ true, + /* transformationApplied */ true, /* callback */ null); + + assertEquals(activity.getCameraCompatControlState(), + CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); + } + + private ICompatCameraControlCallback getCompatCameraControlCallback() { + return new ICompatCameraControlCallback.Stub() { + @Override + public void applyCameraCompatTreatment() {} + + @Override + public void revertCameraCompatTreatment() {} + }; + } + private void assertHasStartingWindow(ActivityRecord atoken) { assertNotNull(atoken.mStartingSurface); assertNotNull(atoken.mStartingData); |