diff options
43 files changed, 1079 insertions, 349 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 93c0c4d7a024..70a23cdf106b 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1139,6 +1139,7 @@ package android.hardware.camera2 { public final class CameraManager { method public String[] getCameraIdListNoLazy() throws android.hardware.camera2.CameraAccessException; method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; + field public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L; // 0xef10e60L } public abstract static class CameraManager.AvailabilityCallback { diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 3bdd39f5d7d7..5291d2b73891 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -29,12 +29,14 @@ import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.app.ActivityThread; import android.app.AppOpsManager; +import android.app.compat.CompatChanges; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.graphics.ImageFormat; import android.graphics.Point; import android.graphics.Rect; import android.graphics.SurfaceTexture; +import android.hardware.camera2.CameraManager; import android.media.AudioAttributes; import android.media.IAudioService; import android.os.Build; @@ -45,6 +47,7 @@ import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemProperties; import android.renderscript.Allocation; import android.renderscript.Element; import android.renderscript.RSIllegalArgumentException; @@ -281,6 +284,14 @@ public class Camera { */ public native static int getNumberOfCameras(); + private static final boolean sLandscapeToPortrait = + SystemProperties.getBoolean(CameraManager.LANDSCAPE_TO_PORTRAIT_PROP, false); + + private static boolean shouldOverrideToPortrait() { + return CompatChanges.isChangeEnabled(CameraManager.OVERRIDE_FRONT_CAMERA_APP_COMPAT) + && sLandscapeToPortrait; + } + /** * Returns the information about a particular camera. * If {@link #getNumberOfCameras()} returns N, the valid id is 0 to N-1. @@ -290,7 +301,9 @@ public class Camera { * low-level failure). */ public static void getCameraInfo(int cameraId, CameraInfo cameraInfo) { - _getCameraInfo(cameraId, cameraInfo); + boolean overrideToPortrait = shouldOverrideToPortrait(); + + _getCameraInfo(cameraId, overrideToPortrait, cameraInfo); IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); IAudioService audioService = IAudioService.Stub.asInterface(b); try { @@ -303,7 +316,8 @@ public class Camera { Log.e(TAG, "Audio service is unavailable for queries"); } } - private native static void _getCameraInfo(int cameraId, CameraInfo cameraInfo); + private native static void _getCameraInfo(int cameraId, boolean overrideToPortrait, + CameraInfo cameraInfo); /** * Information about a camera @@ -484,8 +498,9 @@ public class Camera { mEventHandler = null; } + boolean overrideToPortrait = shouldOverrideToPortrait(); return native_setup(new WeakReference<Camera>(this), cameraId, - ActivityThread.currentOpPackageName()); + ActivityThread.currentOpPackageName(), overrideToPortrait); } /** used by Camera#open, Camera#open(int) */ @@ -555,7 +570,8 @@ public class Camera { } @UnsupportedAppUsage - private native int native_setup(Object cameraThis, int cameraId, String packageName); + private native int native_setup(Object cameraThis, int cameraId, String packageName, + boolean overrideToPortrait); private native final void native_release(); diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index dff2f7ed1cf3..7fed2200d606 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -23,6 +23,10 @@ import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.Disabled; +import android.compat.annotation.Overridable; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.Point; @@ -104,6 +108,24 @@ public final class CameraManager { private final boolean mHasOpenCloseListenerPermission; /** + * Force camera output to be rotated to portrait orientation on landscape cameras. + * Many apps do not handle this situation and display stretched images otherwise. + * @hide + */ + @ChangeId + @Overridable + @Disabled + @TestApi + public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L; + + /** + * System property for allowing the above + * @hide + */ + public static final String LANDSCAPE_TO_PORTRAIT_PROP = + "camera.enable_landscape_to_portrait"; + + /** * @hide */ public CameraManager(Context context) { @@ -520,7 +542,8 @@ public final class CameraManager { for (String physicalCameraId : physicalCameraIds) { CameraMetadataNative physicalCameraInfo = cameraService.getCameraCharacteristics(physicalCameraId, - mContext.getApplicationInfo().targetSdkVersion); + mContext.getApplicationInfo().targetSdkVersion, + /*overrideToPortrait*/false); StreamConfiguration[] configs = physicalCameraInfo.get( CameraCharacteristics. SCALER_PHYSICAL_CAMERA_MULTI_RESOLUTION_STREAM_CONFIGURATIONS); @@ -579,8 +602,9 @@ public final class CameraManager { try { Size displaySize = getDisplaySize(); + boolean overrideToPortrait = shouldOverrideToPortrait(); CameraMetadataNative info = cameraService.getCameraCharacteristics(cameraId, - mContext.getApplicationInfo().targetSdkVersion); + mContext.getApplicationInfo().targetSdkVersion, overrideToPortrait); try { info.setCameraId(Integer.parseInt(cameraId)); } catch (NumberFormatException e) { @@ -697,9 +721,12 @@ public final class CameraManager { ICameraService.ERROR_DISCONNECTED, "Camera service is currently unavailable"); } + + boolean overrideToPortrait = shouldOverrideToPortrait(); cameraUser = cameraService.connectDevice(callbacks, cameraId, - mContext.getOpPackageName(), mContext.getAttributionTag(), uid, - oomScoreOffset, mContext.getApplicationInfo().targetSdkVersion); + mContext.getOpPackageName(), mContext.getAttributionTag(), uid, + oomScoreOffset, mContext.getApplicationInfo().targetSdkVersion, + overrideToPortrait); } catch (ServiceSpecificException e) { if (e.errorCode == ICameraService.ERROR_DEPRECATED_HAL) { throw new AssertionError("Should've gone down the shim path"); @@ -1127,6 +1154,11 @@ public final class CameraManager { return CameraManagerGlobal.get().getTorchStrengthLevel(cameraId); } + private static boolean shouldOverrideToPortrait() { + return CompatChanges.isChangeEnabled(OVERRIDE_FRONT_CAMERA_APP_COMPAT) + && CameraManagerGlobal.sLandscapeToPortrait; + } + /** * A callback for camera devices becoming available or unavailable to open. * @@ -1573,6 +1605,9 @@ public final class CameraManager { public static final boolean sCameraServiceDisabled = SystemProperties.getBoolean("config.disable_cameraservice", false); + public static final boolean sLandscapeToPortrait = + SystemProperties.getBoolean(LANDSCAPE_TO_PORTRAIT_PROP, false); + public static CameraManagerGlobal get() { return gCameraManager; } diff --git a/core/java/android/window/TaskFragmentCreationParams.java b/core/java/android/window/TaskFragmentCreationParams.java index 81ab7836435d..c9ddf92d3740 100644 --- a/core/java/android/window/TaskFragmentCreationParams.java +++ b/core/java/android/window/TaskFragmentCreationParams.java @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.WindowingMode; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.TestApi; import android.graphics.Rect; import android.os.IBinder; @@ -57,14 +58,33 @@ public final class TaskFragmentCreationParams implements Parcelable { /** The initial windowing mode of the TaskFragment. Inherits from parent if not set. */ @WindowingMode - private int mWindowingMode = WINDOWING_MODE_UNDEFINED; + private final int mWindowingMode; + + /** + * The fragment token of the paired primary TaskFragment. + * When it is set, the new TaskFragment will be positioned right above the paired TaskFragment. + * Otherwise, the new TaskFragment will be positioned on the top of the Task by default. + * + * This is different from {@link WindowContainerTransaction#setAdjacentTaskFragments} as we may + * set this when the pair of TaskFragments are stacked, while adjacent is only set on the pair + * of TaskFragments that are in split. + * + * This is needed in case we need to launch a placeholder Activity to split below a transparent + * always-expand Activity. + */ + @Nullable + private final IBinder mPairedPrimaryFragmentToken; private TaskFragmentCreationParams( - @NonNull TaskFragmentOrganizerToken organizer, - @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) { + @NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken, + @NonNull IBinder ownerToken, @NonNull Rect initialBounds, + @WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken) { mOrganizer = organizer; mFragmentToken = fragmentToken; mOwnerToken = ownerToken; + mInitialBounds.set(initialBounds); + mWindowingMode = windowingMode; + mPairedPrimaryFragmentToken = pairedPrimaryFragmentToken; } @NonNull @@ -92,12 +112,22 @@ public final class TaskFragmentCreationParams implements Parcelable { return mWindowingMode; } + /** + * TODO(b/232476698): remove the hide with adding CTS for this in next release. + * @hide + */ + @Nullable + public IBinder getPairedPrimaryFragmentToken() { + return mPairedPrimaryFragmentToken; + } + private TaskFragmentCreationParams(Parcel in) { mOrganizer = TaskFragmentOrganizerToken.CREATOR.createFromParcel(in); mFragmentToken = in.readStrongBinder(); mOwnerToken = in.readStrongBinder(); mInitialBounds.readFromParcel(in); mWindowingMode = in.readInt(); + mPairedPrimaryFragmentToken = in.readStrongBinder(); } /** @hide */ @@ -108,6 +138,7 @@ public final class TaskFragmentCreationParams implements Parcelable { dest.writeStrongBinder(mOwnerToken); mInitialBounds.writeToParcel(dest, flags); dest.writeInt(mWindowingMode); + dest.writeStrongBinder(mPairedPrimaryFragmentToken); } @NonNull @@ -132,6 +163,7 @@ public final class TaskFragmentCreationParams implements Parcelable { + " ownerToken=" + mOwnerToken + " initialBounds=" + mInitialBounds + " windowingMode=" + mWindowingMode + + " pairedFragmentToken=" + mPairedPrimaryFragmentToken + "}"; } @@ -159,6 +191,9 @@ public final class TaskFragmentCreationParams implements Parcelable { @WindowingMode private int mWindowingMode = WINDOWING_MODE_UNDEFINED; + @Nullable + private IBinder mPairedPrimaryFragmentToken; + public Builder(@NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) { mOrganizer = organizer; @@ -180,14 +215,29 @@ public final class TaskFragmentCreationParams implements Parcelable { return this; } + /** + * Sets the fragment token of the paired primary TaskFragment. + * When it is set, the new TaskFragment will be positioned right above the paired + * TaskFragment. Otherwise, the new TaskFragment will be positioned on the top of the Task + * by default. + * + * This is needed in case we need to launch a placeholder Activity to split below a + * transparent always-expand Activity. + * + * TODO(b/232476698): remove the hide with adding CTS for this in next release. + * @hide + */ + @NonNull + public Builder setPairedPrimaryFragmentToken(@Nullable IBinder fragmentToken) { + mPairedPrimaryFragmentToken = fragmentToken; + return this; + } + /** Constructs the options to create TaskFragment with. */ @NonNull public TaskFragmentCreationParams build() { - final TaskFragmentCreationParams result = new TaskFragmentCreationParams( - mOrganizer, mFragmentToken, mOwnerToken); - result.mInitialBounds.set(mInitialBounds); - result.mWindowingMode = mWindowingMode; - return result; + return new TaskFragmentCreationParams(mOrganizer, mFragmentToken, mOwnerToken, + mInitialBounds, mWindowingMode, mPairedPrimaryFragmentToken); } } } diff --git a/core/jni/android_hardware_Camera.cpp b/core/jni/android_hardware_Camera.cpp index 365a18d174c9..a8abe50a9755 100644 --- a/core/jni/android_hardware_Camera.cpp +++ b/core/jni/android_hardware_Camera.cpp @@ -529,9 +529,8 @@ static jint android_hardware_Camera_getNumberOfCameras(JNIEnv *env, jobject thiz return Camera::getNumberOfCameras(); } -static void android_hardware_Camera_getCameraInfo(JNIEnv *env, jobject thiz, - jint cameraId, jobject info_obj) -{ +static void android_hardware_Camera_getCameraInfo(JNIEnv *env, jobject thiz, jint cameraId, + jboolean overrideToPortrait, jobject info_obj) { CameraInfo cameraInfo; if (cameraId >= Camera::getNumberOfCameras() || cameraId < 0) { ALOGE("%s: Unknown camera ID %d", __FUNCTION__, cameraId); @@ -539,7 +538,7 @@ static void android_hardware_Camera_getCameraInfo(JNIEnv *env, jobject thiz, return; } - status_t rc = Camera::getCameraInfo(cameraId, &cameraInfo); + status_t rc = Camera::getCameraInfo(cameraId, overrideToPortrait, &cameraInfo); if (rc != NO_ERROR) { jniThrowRuntimeException(env, "Fail to get camera info"); return; @@ -555,9 +554,9 @@ static void android_hardware_Camera_getCameraInfo(JNIEnv *env, jobject thiz, } // connect to camera service -static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, - jobject weak_this, jint cameraId, jstring clientPackageName) -{ +static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, jobject weak_this, + jint cameraId, jstring clientPackageName, + jboolean overrideToPortrait) { // Convert jstring to String16 const char16_t *rawClientName = reinterpret_cast<const char16_t*>( env->GetStringChars(clientPackageName, NULL)); @@ -567,8 +566,9 @@ static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, reinterpret_cast<const jchar*>(rawClientName)); int targetSdkVersion = android_get_application_target_sdk_version(); - sp<Camera> camera = Camera::connect(cameraId, clientName, Camera::USE_CALLING_UID, - Camera::USE_CALLING_PID, targetSdkVersion); + sp<Camera> camera = + Camera::connect(cameraId, clientName, Camera::USE_CALLING_UID, Camera::USE_CALLING_PID, + targetSdkVersion, overrideToPortrait); if (camera == NULL) { return -EACCES; } @@ -596,7 +596,7 @@ static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, // Update default display orientation in case the sensor is reverse-landscape CameraInfo cameraInfo; - status_t rc = Camera::getCameraInfo(cameraId, &cameraInfo); + status_t rc = Camera::getCameraInfo(cameraId, overrideToPortrait, &cameraInfo); if (rc != NO_ERROR) { ALOGE("%s: getCameraInfo error: %d", __FUNCTION__, rc); return rc; @@ -1051,93 +1051,43 @@ static int32_t android_hardware_Camera_getAudioRestriction( //------------------------------------------------- static const JNINativeMethod camMethods[] = { - { "getNumberOfCameras", - "()I", - (void *)android_hardware_Camera_getNumberOfCameras }, - { "_getCameraInfo", - "(ILandroid/hardware/Camera$CameraInfo;)V", - (void*)android_hardware_Camera_getCameraInfo }, - { "native_setup", - "(Ljava/lang/Object;ILjava/lang/String;)I", - (void*)android_hardware_Camera_native_setup }, - { "native_release", - "()V", - (void*)android_hardware_Camera_release }, - { "setPreviewSurface", - "(Landroid/view/Surface;)V", - (void *)android_hardware_Camera_setPreviewSurface }, - { "setPreviewTexture", - "(Landroid/graphics/SurfaceTexture;)V", - (void *)android_hardware_Camera_setPreviewTexture }, - { "setPreviewCallbackSurface", - "(Landroid/view/Surface;)V", - (void *)android_hardware_Camera_setPreviewCallbackSurface }, - { "startPreview", - "()V", - (void *)android_hardware_Camera_startPreview }, - { "_stopPreview", - "()V", - (void *)android_hardware_Camera_stopPreview }, - { "previewEnabled", - "()Z", - (void *)android_hardware_Camera_previewEnabled }, - { "setHasPreviewCallback", - "(ZZ)V", - (void *)android_hardware_Camera_setHasPreviewCallback }, - { "_addCallbackBuffer", - "([BI)V", - (void *)android_hardware_Camera_addCallbackBuffer }, - { "native_autoFocus", - "()V", - (void *)android_hardware_Camera_autoFocus }, - { "native_cancelAutoFocus", - "()V", - (void *)android_hardware_Camera_cancelAutoFocus }, - { "native_takePicture", - "(I)V", - (void *)android_hardware_Camera_takePicture }, - { "native_setParameters", - "(Ljava/lang/String;)V", - (void *)android_hardware_Camera_setParameters }, - { "native_getParameters", - "()Ljava/lang/String;", - (void *)android_hardware_Camera_getParameters }, - { "reconnect", - "()V", - (void*)android_hardware_Camera_reconnect }, - { "lock", - "()V", - (void*)android_hardware_Camera_lock }, - { "unlock", - "()V", - (void*)android_hardware_Camera_unlock }, - { "startSmoothZoom", - "(I)V", - (void *)android_hardware_Camera_startSmoothZoom }, - { "stopSmoothZoom", - "()V", - (void *)android_hardware_Camera_stopSmoothZoom }, - { "setDisplayOrientation", - "(I)V", - (void *)android_hardware_Camera_setDisplayOrientation }, - { "_enableShutterSound", - "(Z)Z", - (void *)android_hardware_Camera_enableShutterSound }, - { "_startFaceDetection", - "(I)V", - (void *)android_hardware_Camera_startFaceDetection }, - { "_stopFaceDetection", - "()V", - (void *)android_hardware_Camera_stopFaceDetection}, - { "enableFocusMoveCallback", - "(I)V", - (void *)android_hardware_Camera_enableFocusMoveCallback}, - { "setAudioRestriction", - "(I)V", - (void *)android_hardware_Camera_setAudioRestriction}, - { "getAudioRestriction", - "()I", - (void *)android_hardware_Camera_getAudioRestriction}, + {"getNumberOfCameras", "()I", (void *)android_hardware_Camera_getNumberOfCameras}, + {"_getCameraInfo", "(IZLandroid/hardware/Camera$CameraInfo;)V", + (void *)android_hardware_Camera_getCameraInfo}, + {"native_setup", "(Ljava/lang/Object;ILjava/lang/String;Z)I", + (void *)android_hardware_Camera_native_setup}, + {"native_release", "()V", (void *)android_hardware_Camera_release}, + {"setPreviewSurface", "(Landroid/view/Surface;)V", + (void *)android_hardware_Camera_setPreviewSurface}, + {"setPreviewTexture", "(Landroid/graphics/SurfaceTexture;)V", + (void *)android_hardware_Camera_setPreviewTexture}, + {"setPreviewCallbackSurface", "(Landroid/view/Surface;)V", + (void *)android_hardware_Camera_setPreviewCallbackSurface}, + {"startPreview", "()V", (void *)android_hardware_Camera_startPreview}, + {"_stopPreview", "()V", (void *)android_hardware_Camera_stopPreview}, + {"previewEnabled", "()Z", (void *)android_hardware_Camera_previewEnabled}, + {"setHasPreviewCallback", "(ZZ)V", (void *)android_hardware_Camera_setHasPreviewCallback}, + {"_addCallbackBuffer", "([BI)V", (void *)android_hardware_Camera_addCallbackBuffer}, + {"native_autoFocus", "()V", (void *)android_hardware_Camera_autoFocus}, + {"native_cancelAutoFocus", "()V", (void *)android_hardware_Camera_cancelAutoFocus}, + {"native_takePicture", "(I)V", (void *)android_hardware_Camera_takePicture}, + {"native_setParameters", "(Ljava/lang/String;)V", + (void *)android_hardware_Camera_setParameters}, + {"native_getParameters", "()Ljava/lang/String;", + (void *)android_hardware_Camera_getParameters}, + {"reconnect", "()V", (void *)android_hardware_Camera_reconnect}, + {"lock", "()V", (void *)android_hardware_Camera_lock}, + {"unlock", "()V", (void *)android_hardware_Camera_unlock}, + {"startSmoothZoom", "(I)V", (void *)android_hardware_Camera_startSmoothZoom}, + {"stopSmoothZoom", "()V", (void *)android_hardware_Camera_stopSmoothZoom}, + {"setDisplayOrientation", "(I)V", (void *)android_hardware_Camera_setDisplayOrientation}, + {"_enableShutterSound", "(Z)Z", (void *)android_hardware_Camera_enableShutterSound}, + {"_startFaceDetection", "(I)V", (void *)android_hardware_Camera_startFaceDetection}, + {"_stopFaceDetection", "()V", (void *)android_hardware_Camera_stopFaceDetection}, + {"enableFocusMoveCallback", "(I)V", + (void *)android_hardware_Camera_enableFocusMoveCallback}, + {"setAudioRestriction", "(I)V", (void *)android_hardware_Camera_setAudioRestriction}, + {"getAudioRestriction", "()I", (void *)android_hardware_Camera_getAudioRestriction}, }; struct field { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index d7d43aa19757..b910287aa535 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -133,8 +133,18 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { } // Create a TaskFragment for the secondary activity. - createTaskFragmentAndStartActivity(wct, secondaryFragmentToken, ownerToken, - secondaryFragmentBounds, windowingMode, activityIntent, + final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder( + getOrganizerToken(), secondaryFragmentToken, ownerToken) + .setInitialBounds(secondaryFragmentBounds) + .setWindowingMode(windowingMode) + // Make sure to set the paired fragment token so that the new TaskFragment will be + // positioned right above the paired TaskFragment. + // This is needed in case we need to launch a placeholder Activity to split below a + // transparent always-expand Activity. + .setPairedPrimaryFragmentToken(launchingFragmentToken) + .build(); + createTaskFragment(wct, fragmentOptions); + wct.startActivityInTaskFragment(secondaryFragmentToken, ownerToken, activityIntent, activityOptions); // Set adjacent to each other so that the containers below will be invisible. @@ -173,8 +183,21 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { */ void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { - final TaskFragmentCreationParams fragmentOptions = - createFragmentOptions(fragmentToken, ownerToken, bounds, windowingMode); + final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder( + getOrganizerToken(), fragmentToken, ownerToken) + .setInitialBounds(bounds) + .setWindowingMode(windowingMode) + .build(); + createTaskFragment(wct, fragmentOptions); + } + + void createTaskFragment(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentCreationParams fragmentOptions) { + if (mFragmentInfos.containsKey(fragmentOptions.getFragmentToken())) { + throw new IllegalArgumentException( + "There is an existing TaskFragment with fragmentToken=" + + fragmentOptions.getFragmentToken()); + } wct.createTaskFragment(fragmentOptions); } @@ -189,18 +212,6 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken()); } - /** - * @param ownerToken The token of the activity that creates this task fragment. It does not - * have to be a child of this task fragment, but must belong to the same task. - */ - private void createTaskFragmentAndStartActivity(@NonNull WindowContainerTransaction wct, - @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds, - @WindowingMode int windowingMode, @NonNull Intent activityIntent, - @Nullable Bundle activityOptions) { - createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode); - wct.startActivityInTaskFragment(fragmentToken, ownerToken, activityIntent, activityOptions); - } - void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary, @Nullable IBinder secondary, @Nullable SplitRule splitRule) { WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = null; @@ -238,22 +249,6 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { wct.setCompanionTaskFragment(secondary, finishSecondaryWithPrimary ? primary : null); } - TaskFragmentCreationParams createFragmentOptions(@NonNull IBinder fragmentToken, - @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { - if (mFragmentInfos.containsKey(fragmentToken)) { - throw new IllegalArgumentException( - "There is an existing TaskFragment with fragmentToken=" + fragmentToken); - } - - return new TaskFragmentCreationParams.Builder( - getOrganizerToken(), - fragmentToken, - ownerToken) - .setInitialBounds(bounds) - .setWindowingMode(windowingMode) - .build(); - } - void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @Nullable Rect bounds) { if (!mFragmentInfos.containsKey(fragmentToken)) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 6f9a4ff8188a..d3dc05fb92d4 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -1218,14 +1218,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, @NonNull Activity activityInTask, int taskId) { return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */, - activityInTask, taskId); + activityInTask, taskId, null /* pairedPrimaryContainer */); } @GuardedBy("mLock") TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent, - activityInTask, taskId); + activityInTask, taskId, null /* pairedPrimaryContainer */); } /** @@ -1237,10 +1237,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * @param activityInTask activity in the same Task so that we can get the Task bounds * if needed. * @param taskId parent Task of the new TaskFragment. + * @param pairedPrimaryContainer the paired primary {@link TaskFragmentContainer}. When it is + * set, the new container will be added right above it. */ @GuardedBy("mLock") TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, - @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { + @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId, + @Nullable TaskFragmentContainer pairedPrimaryContainer) { if (activityInTask == null) { throw new IllegalArgumentException("activityInTask must not be null,"); } @@ -1249,7 +1252,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } final TaskContainer taskContainer = mTaskContainers.get(taskId); final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity, - pendingAppearedIntent, taskContainer, this); + pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer); return container; } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 5395fb2ef5ed..47253d388f0d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -36,6 +36,7 @@ import android.util.Size; import android.view.View; import android.view.WindowInsets; import android.view.WindowMetrics; +import android.window.TaskFragmentCreationParams; import android.window.WindowContainerTransaction; import androidx.annotation.GuardedBy; @@ -307,10 +308,13 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } final int taskId = primaryContainer.getTaskId(); - final TaskFragmentContainer secondaryContainer = mController.newContainer(activityIntent, - launchingActivity, taskId); - final int windowingMode = mController.getTaskContainer(taskId) - .getWindowingModeForSplitTaskFragment(primaryRectBounds); + final TaskFragmentContainer secondaryContainer = mController.newContainer( + null /* pendingAppearedActivity */, activityIntent, launchingActivity, taskId, + // Pass in the primary container to make sure it is added right above the primary. + primaryContainer); + final TaskContainer taskContainer = mController.getTaskContainer(taskId); + final int windowingMode = taskContainer.getWindowingModeForSplitTaskFragment( + primaryRectBounds); mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer, rule, splitAttributes); startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds, @@ -412,17 +416,18 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } @Override - void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, - @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { - final TaskFragmentContainer container = mController.getContainer(fragmentToken); + void createTaskFragment(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentCreationParams fragmentOptions) { + final TaskFragmentContainer container = mController.getContainer( + fragmentOptions.getFragmentToken()); if (container == null) { throw new IllegalStateException( "Creating a task fragment that is not registered with controller."); } - container.setLastRequestedBounds(bounds); - container.setLastRequestedWindowingMode(windowingMode); - super.createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode); + container.setLastRequestedBounds(fragmentOptions.getInitialBounds()); + container.setLastRequestedWindowingMode(fragmentOptions.getWindowingMode()); + super.createTaskFragment(wct, fragmentOptions); } @Override diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index e31792ad718f..fcf0ac78af38 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -118,10 +118,12 @@ class TaskFragmentContainer { /** * Creates a container with an existing activity that will be re-parented to it in a window * container transaction. + * @param pairedPrimaryContainer when it is set, the new container will be add right above it */ TaskFragmentContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @NonNull TaskContainer taskContainer, - @NonNull SplitController controller) { + @NonNull SplitController controller, + @Nullable TaskFragmentContainer pairedPrimaryContainer) { if ((pendingAppearedActivity == null && pendingAppearedIntent == null) || (pendingAppearedActivity != null && pendingAppearedIntent != null)) { throw new IllegalArgumentException( @@ -130,7 +132,16 @@ class TaskFragmentContainer { mController = controller; mToken = new Binder("TaskFragmentContainer"); mTaskContainer = taskContainer; - taskContainer.mContainers.add(this); + if (pairedPrimaryContainer != null) { + if (pairedPrimaryContainer.getTaskContainer() != taskContainer) { + throw new IllegalArgumentException( + "pairedPrimaryContainer must be in the same Task"); + } + final int primaryIndex = taskContainer.mContainers.indexOf(pairedPrimaryContainer); + taskContainer.mContainers.add(primaryIndex + 1, this); + } else { + taskContainer.mContainers.add(this); + } if (pendingAppearedActivity != null) { addPendingAppearedActivity(pendingAppearedActivity); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index 31aa09c902b1..bbb454d31c38 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -102,7 +102,7 @@ public class JetpackTaskFragmentOrganizerTest { public void testExpandTaskFragment() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mSplitController); + new Intent(), taskContainer, mSplitController, null /* pairedPrimaryContainer */); final TaskFragmentInfo info = createMockInfo(container); mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info); container.setInfo(mTransaction, info); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 221c7640ef0b..6725dfdb94e8 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -169,7 +169,7 @@ public class SplitControllerTest { final TaskContainer taskContainer = createTestTaskContainer(); // tf1 has no running activity so is not active. final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mSplitController); + new Intent(), taskContainer, mSplitController, null /* pairedPrimaryContainer */); // tf2 has running activity so is active. final TaskFragmentContainer tf2 = mock(TaskFragmentContainer.class); doReturn(1).when(tf2).getRunningActivityCount(); @@ -375,7 +375,7 @@ public class SplitControllerTest { final Intent intent = new Intent(); final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - intent, taskContainer, mSplitController); + intent, taskContainer, mSplitController, null /* pairedPrimaryContainer */); final SplitController.ActivityStartMonitor monitor = mSplitController.getActivityStartMonitor(); @@ -609,7 +609,7 @@ public class SplitControllerTest { false /* isOnReparent */); assertFalse(result); - verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any()); } @Test @@ -771,7 +771,7 @@ public class SplitControllerTest { false /* isOnReparent */); assertTrue(result); - verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any()); verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @@ -813,7 +813,7 @@ public class SplitControllerTest { false /* isOnReparent */); assertTrue(result); - verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any()); verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java index 95328ce700e3..13e709271221 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -122,7 +122,7 @@ public class TaskContainerTest { assertTrue(taskContainer.isEmpty()); final TaskFragmentContainer tf = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mController); + new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */); assertFalse(taskContainer.isEmpty()); @@ -138,11 +138,11 @@ public class TaskContainerTest { assertNull(taskContainer.getTopTaskFragmentContainer()); final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mController); + new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */); assertEquals(tf0, taskContainer.getTopTaskFragmentContainer()); final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mController); + new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */); assertEquals(tf1, taskContainer.getTopTaskFragmentContainer()); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index 99f56b4ceb17..5c3ba72e2361 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -94,18 +94,21 @@ public class TaskFragmentContainerTest { // One of the activity and the intent must be non-null assertThrows(IllegalArgumentException.class, - () -> new TaskFragmentContainer(null, null, taskContainer, mController)); + () -> new TaskFragmentContainer(null, null, taskContainer, mController, + null /* pairedPrimaryContainer */)); // One of the activity and the intent must be null. assertThrows(IllegalArgumentException.class, - () -> new TaskFragmentContainer(mActivity, mIntent, taskContainer, mController)); + () -> new TaskFragmentContainer(mActivity, mIntent, taskContainer, mController, + null /* pairedPrimaryContainer */)); } @Test public void testFinish() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController); + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); doReturn(container).when(mController).getContainerWithActivity(mActivity); // Only remove the activity, but not clear the reference until appeared. @@ -137,12 +140,14 @@ public class TaskFragmentContainerTest { public void testFinish_notFinishActivityThatIsReparenting() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController); + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); final TaskFragmentInfo info = createMockTaskFragmentInfo(container0, mActivity); container0.setInfo(mTransaction, info); // Request to reparent the activity to a new TaskFragment. final TaskFragmentContainer container1 = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController); + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); doReturn(container1).when(mController).getContainerWithActivity(mActivity); final WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -159,7 +164,8 @@ public class TaskFragmentContainerTest { final TaskContainer taskContainer = createTestTaskContainer(); // Pending activity should be cleared when it has appeared on server side. final TaskFragmentContainer pendingActivityContainer = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController); + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); assertTrue(pendingActivityContainer.mPendingAppearedActivities.contains( mActivity.getActivityToken())); @@ -172,7 +178,8 @@ public class TaskFragmentContainerTest { // Pending intent should be cleared when the container becomes non-empty. final TaskFragmentContainer pendingIntentContainer = new TaskFragmentContainer( - null /* pendingAppearedActivity */, mIntent, taskContainer, mController); + null /* pendingAppearedActivity */, mIntent, taskContainer, mController, + null /* pairedPrimaryContainer */); assertEquals(mIntent, pendingIntentContainer.getPendingAppearedIntent()); @@ -187,7 +194,7 @@ public class TaskFragmentContainerTest { public void testIsWaitingActivityAppear() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); assertTrue(container.isWaitingActivityAppear()); @@ -209,7 +216,7 @@ public class TaskFragmentContainerTest { doNothing().when(mController).onTaskFragmentAppearEmptyTimeout(any(), any()); final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); assertNull(container.mAppearEmptyTimeout); @@ -249,7 +256,7 @@ public class TaskFragmentContainerTest { public void testCollectNonFinishingActivities() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); List<Activity> activities = container.collectNonFinishingActivities(); assertTrue(activities.isEmpty()); @@ -277,7 +284,7 @@ public class TaskFragmentContainerTest { public void testAddPendingActivity() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); container.addPendingAppearedActivity(mActivity); assertEquals(1, container.collectNonFinishingActivities().size()); @@ -291,9 +298,9 @@ public class TaskFragmentContainerTest { public void testIsAbove() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container0 = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); final TaskFragmentContainer container1 = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); assertTrue(container1.isAbove(container0)); assertFalse(container0.isAbove(container1)); @@ -303,7 +310,7 @@ public class TaskFragmentContainerTest { public void testGetBottomMostActivity() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); container.addPendingAppearedActivity(mActivity); assertEquals(mActivity, container.getBottomMostActivity()); @@ -320,7 +327,7 @@ public class TaskFragmentContainerTest { public void testOnActivityDestroyed() { final TaskContainer taskContainer = createTestTaskContainer(mController); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); container.addPendingAppearedActivity(mActivity); final List<IBinder> activities = new ArrayList<>(); activities.add(mActivity.getActivityToken()); @@ -340,7 +347,7 @@ public class TaskFragmentContainerTest { // True if no info set. final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); spyOn(taskContainer); doReturn(true).when(taskContainer).isVisible(); @@ -403,7 +410,7 @@ public class TaskFragmentContainerTest { public void testHasAppearedActivity() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); container.addPendingAppearedActivity(mActivity); assertFalse(container.hasAppearedActivity(mActivity.getActivityToken())); @@ -420,7 +427,7 @@ public class TaskFragmentContainerTest { public void testHasPendingAppearedActivity() { final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); container.addPendingAppearedActivity(mActivity); assertTrue(container.hasPendingAppearedActivity(mActivity.getActivityToken())); @@ -437,9 +444,9 @@ public class TaskFragmentContainerTest { public void testHasActivity() { final TaskContainer taskContainer = createTestTaskContainer(mController); final TaskFragmentContainer container1 = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); final TaskFragmentContainer container2 = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); // Activity is pending appeared on container2. container2.addPendingAppearedActivity(mActivity); @@ -472,6 +479,27 @@ public class TaskFragmentContainerTest { assertTrue(container2.hasActivity(mActivity.getActivityToken())); } + @Test + public void testNewContainerWithPairedPrimaryContainer() { + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer tf0 = new TaskFragmentContainer( + null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, + null /* pairedPrimaryTaskFragment */); + final TaskFragmentContainer tf1 = new TaskFragmentContainer( + null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, + null /* pairedPrimaryTaskFragment */); + taskContainer.mContainers.add(tf0); + taskContainer.mContainers.add(tf1); + + // When tf2 is created with using tf0 as pairedPrimaryContainer, tf2 should be inserted + // right above tf0. + final TaskFragmentContainer tf2 = new TaskFragmentContainer( + null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, tf0); + assertEquals(0, taskContainer.indexOf(tf0)); + assertEquals(1, taskContainer.indexOf(tf2)); + assertEquals(2, taskContainer.indexOf(tf1)); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { final Activity activity = mock(Activity.class); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index ad0adc6c84c4..c743582c3264 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -692,7 +692,7 @@ public abstract class WMShellBaseModule { // Use optional-of-lazy for the dependency that this provider relies on. // Lazy ensures that this provider will not be the cause the dependency is created // when it will not be returned due to the condition below. - if (DesktopModeStatus.IS_SUPPORTED) { + if (DesktopModeStatus.isProto1Enabled()) { return desktopModeController.map(Lazy::get); } return Optional.empty(); @@ -709,7 +709,7 @@ public abstract class WMShellBaseModule { // Use optional-of-lazy for the dependency that this provider relies on. // Lazy ensures that this provider will not be the cause the dependency is created // when it will not be returned due to the condition below. - if (DesktopModeStatus.IS_SUPPORTED) { + if (DesktopModeStatus.isAnyEnabled()) { return desktopModeTaskRepository.map(Lazy::get); } return Optional.empty(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java index 5824f51a1d33..7eb01a79f14f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -100,7 +100,7 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll mDesktopModeTaskRepository = desktopModeTaskRepository; mMainExecutor = mainExecutor; mSettingsObserver = new SettingsObserver(mContext, mainHandler); - if (DesktopModeStatus.isSupported()) { + if (DesktopModeStatus.isProto1Enabled()) { shellInit.addInitCallback(this::onInit, this); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java index e3eb2b746969..67f4a1914c49 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java @@ -33,17 +33,38 @@ public class DesktopModeStatus { /** * Flag to indicate whether desktop mode is available on the device */ - public static final boolean IS_SUPPORTED = SystemProperties.getBoolean( + private static final boolean IS_SUPPORTED = SystemProperties.getBoolean( "persist.wm.debug.desktop_mode", false); /** + * Flag to indicate whether desktop mode proto 2 is available on the device + */ + private static final boolean IS_PROTO2_ENABLED = SystemProperties.getBoolean( + "persist.wm.debug.desktop_mode_2", false); + + /** * Return {@code true} if desktop mode support is enabled */ - public static boolean isSupported() { + public static boolean isProto1Enabled() { return IS_SUPPORTED; } /** + * Return {@code true} is desktop windowing proto 2 is enabled + */ + public static boolean isProto2Enabled() { + return IS_PROTO2_ENABLED; + } + + /** + * Return {@code true} if proto 1 or 2 is enabled. + * Can be used to guard logic that is common for both prototypes. + */ + public static boolean isAnyEnabled() { + return isProto1Enabled() || isProto2Enabled(); + } + + /** * Check if desktop mode is active * * @return {@code true} if active @@ -61,5 +82,4 @@ public class DesktopModeStatus { return false; } } - } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index 8a9b74fd72b1..793bad86d873 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -68,7 +68,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, private void onInit() { mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FREEFORM); - if (DesktopModeStatus.IS_SUPPORTED) { + if (DesktopModeStatus.isAnyEnabled()) { mShellTaskOrganizer.addFocusListener(this); } } @@ -90,7 +90,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, t.apply(); } - if (DesktopModeStatus.IS_SUPPORTED) { + if (DesktopModeStatus.isAnyEnabled()) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.addOrMoveFreeformTaskToTop(taskInfo.taskId); if (taskInfo.isVisible) { @@ -110,7 +110,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, taskInfo.taskId); mTasks.remove(taskInfo.taskId); - if (DesktopModeStatus.IS_SUPPORTED) { + if (DesktopModeStatus.isAnyEnabled()) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.removeFreeformTask(taskInfo.taskId); if (repository.removeActiveTask(taskInfo.taskId)) { @@ -134,7 +134,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, taskInfo.taskId); mWindowDecorationViewModel.onTaskInfoChanged(state.mTaskInfo); - if (DesktopModeStatus.IS_SUPPORTED) { + if (DesktopModeStatus.isAnyEnabled()) { mDesktopModeTaskRepository.ifPresent(repository -> { if (taskInfo.isVisible) { if (repository.addActiveTask(taskInfo.taskId)) { @@ -152,7 +152,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Focus Changed: #%d focused=%b", taskInfo.taskId, taskInfo.isFocused); - if (DesktopModeStatus.IS_SUPPORTED && taskInfo.isFocused) { + if (DesktopModeStatus.isAnyEnabled() && taskInfo.isFocused) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.addOrMoveFreeformTaskToTop(taskInfo.taskId); }); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 56554020f3cc..afefd5dc6344 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -517,7 +517,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true; - return DesktopModeStatus.IS_SUPPORTED + return DesktopModeStatus.isAnyEnabled() && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD && mDisplayController.getDisplayContext(taskInfo.displayId) .getResources().getConfiguration().smallestScreenWidthDp >= 600; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java index dad913300711..b3c9e238a614 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java @@ -100,7 +100,7 @@ public class DesktopModeControllerTest extends ShellTestCase { @Before public void setUp() { mMockitoSession = mockitoSession().mockStatic(DesktopModeStatus.class).startMocking(); - when(DesktopModeStatus.isSupported()).thenReturn(true); + when(DesktopModeStatus.isProto1Enabled()).thenReturn(true); when(DesktopModeStatus.isActive(any())).thenReturn(true); mShellInit = Mockito.spy(new ShellInit(mTestExecutor)); @@ -129,7 +129,7 @@ public class DesktopModeControllerTest extends ShellTestCase { @Test public void instantiate_flagOff_doNotAddInitCallback() { - when(DesktopModeStatus.isSupported()).thenReturn(false); + when(DesktopModeStatus.isProto1Enabled()).thenReturn(false); clearInvocations(mShellInit); createController(); diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java index a2eae2c9579a..2b7bcbee79fd 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java @@ -85,7 +85,8 @@ public class CameraBinderTest extends AndroidTestCase { public void testCameraInfo() throws Exception { for (int cameraId = 0; cameraId < mUtils.getGuessedNumCameras(); ++cameraId) { - CameraInfo info = mUtils.getCameraService().getCameraInfo(cameraId); + CameraInfo info = mUtils.getCameraService().getCameraInfo(cameraId, + /*overrideToPortrait*/false); assertTrue("Facing was not set for camera " + cameraId, info.info.facing != -1); assertTrue("Orientation was not set for camera " + cameraId, info.info.orientation != -1); @@ -159,7 +160,8 @@ public class CameraBinderTest extends AndroidTestCase { .connect(dummyCallbacks, cameraId, clientPackageName, ICameraService.USE_CALLING_UID, ICameraService.USE_CALLING_PID, - getContext().getApplicationInfo().targetSdkVersion); + getContext().getApplicationInfo().targetSdkVersion, + /*overrideToPortrait*/false); assertNotNull(String.format("Camera %s was null", cameraId), cameraUser); Log.v(TAG, String.format("Camera %s connected", cameraId)); @@ -264,7 +266,8 @@ public class CameraBinderTest extends AndroidTestCase { dummyCallbacks, String.valueOf(cameraId), clientPackageName, clientAttributionTag, ICameraService.USE_CALLING_UID, 0 /*oomScoreOffset*/, - getContext().getApplicationInfo().targetSdkVersion); + getContext().getApplicationInfo().targetSdkVersion, + /*overrideToPortrait*/false); assertNotNull(String.format("Camera %s was null", cameraId), cameraUser); Log.v(TAG, String.format("Camera %s connected", cameraId)); diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java index 0890346db198..9d09dcc5c440 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java @@ -244,7 +244,8 @@ public class CameraDeviceBinderTest extends AndroidTestCase { mCameraUser = mUtils.getCameraService().connectDevice(mMockCb, mCameraId, clientPackageName, clientAttributionTag, ICameraService.USE_CALLING_UID, - /*oomScoreOffset*/0, getContext().getApplicationInfo().targetSdkVersion); + /*oomScoreOffset*/0, getContext().getApplicationInfo().targetSdkVersion, + /*overrideToPortrait*/false); assertNotNull(String.format("Camera %s was null", mCameraId), mCameraUser); mHandlerThread = new HandlerThread(TAG); mHandlerThread.start(); @@ -417,7 +418,7 @@ public class CameraDeviceBinderTest extends AndroidTestCase { @SmallTest public void testCameraCharacteristics() throws RemoteException { CameraMetadataNative info = mUtils.getCameraService().getCameraCharacteristics(mCameraId, - getContext().getApplicationInfo().targetSdkVersion); + getContext().getApplicationInfo().targetSdkVersion, /*overrideToPortrait*/false); assertFalse(info.isEmpty()); assertNotNull(info.get(CameraCharacteristics.SCALER_AVAILABLE_FORMATS)); diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java index c9ea79432360..5883b6c0e723 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java @@ -87,8 +87,8 @@ public class PreviewPositionHelper { taskPercent = mDesiredStagePosition != STAGE_POSITION_TOP_OR_LEFT ? mSplitBounds.topTaskPercent : (1 - (mSplitBounds.topTaskPercent + mSplitBounds.dividerHeightPercent)); - // Scale portrait height to that of (actual screen - taskbar inset) - fullscreenTaskHeight = (screenHeightPx) * taskPercent; + // Scale portrait height to that of the actual screen + fullscreenTaskHeight = screenHeightPx * taskPercent; if (mTaskbarInApp) { canvasScreenRatio = canvasHeight / fullscreenTaskHeight; } else { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 5a81bd3e01b6..419cf1faf1fd 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -96,6 +96,7 @@ import java.util.Set; import java.util.concurrent.Executor; import javax.inject.Inject; +import javax.inject.Provider; import kotlin.Unit; @@ -714,7 +715,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull SystemUIDialogManager dialogManager, @NonNull LatencyTracker latencyTracker, @NonNull ActivityLaunchAnimator activityLaunchAnimator, - @NonNull Optional<AlternateUdfpsTouchProvider> alternateTouchProvider, + @NonNull Optional<Provider<AlternateUdfpsTouchProvider>> alternateTouchProvider, @NonNull @BiometricsBackground Executor biometricsExecutor, @NonNull PrimaryBouncerInteractor primaryBouncerInteractor, @NonNull SinglePointerTouchProcessor singlePointerTouchProcessor) { @@ -746,7 +747,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; mLatencyTracker = latencyTracker; mActivityLaunchAnimator = activityLaunchAnimator; - mAlternateTouchProvider = alternateTouchProvider.orElse(null); + mAlternateTouchProvider = alternateTouchProvider.map(Provider::get).orElse(null); mSensorProps = new FingerprintSensorPropertiesInternal( -1 /* sensorId */, SensorProperties.STRENGTH_CONVENIENCE, diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt index 77d0496e43db..27466d4f58bc 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt @@ -19,7 +19,7 @@ package com.android.systemui.controls.dagger import android.content.Context import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT -import com.android.systemui.controls.ControlsSettingsRepository +import com.android.systemui.controls.settings.ControlsSettingsRepository import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.ControlsTileResourceConfiguration import com.android.systemui.controls.controller.ControlsTileResourceConfigurationImpl diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt index 9ae605e30a83..6d6410de1a91 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt @@ -20,8 +20,8 @@ import android.app.Activity import android.content.pm.PackageManager import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsMetricsLoggerImpl -import com.android.systemui.controls.ControlsSettingsRepository -import com.android.systemui.controls.ControlsSettingsRepositoryImpl +import com.android.systemui.controls.settings.ControlsSettingsRepository +import com.android.systemui.controls.settings.ControlsSettingsRepositoryImpl import com.android.systemui.controls.controller.ControlsBindingController import com.android.systemui.controls.controller.ControlsBindingControllerImpl import com.android.systemui.controls.controller.ControlsController @@ -34,6 +34,8 @@ import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsListingControllerImpl import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.controls.management.ControlsRequestDialog +import com.android.systemui.controls.settings.ControlsSettingsDialogManager +import com.android.systemui.controls.settings.ControlsSettingsDialogManagerImpl import com.android.systemui.controls.ui.ControlActionCoordinator import com.android.systemui.controls.ui.ControlActionCoordinatorImpl import com.android.systemui.controls.ui.ControlsActivity @@ -90,6 +92,11 @@ abstract class ControlsModule { ): ControlsSettingsRepository @Binds + abstract fun provideDialogManager( + manager: ControlsSettingsDialogManagerImpl + ): ControlsSettingsDialogManager + + @Binds abstract fun provideMetricsLogger(logger: ControlsMetricsLoggerImpl): ControlsMetricsLogger @Binds diff --git a/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsDialogManager.kt b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsDialogManager.kt new file mode 100644 index 000000000000..bb2e2d701aa0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsDialogManager.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.controls.settings + +import android.app.AlertDialog +import android.content.Context +import android.content.Context.MODE_PRIVATE +import android.content.DialogInterface +import android.content.SharedPreferences +import android.provider.Settings +import androidx.annotation.VisibleForTesting +import com.android.systemui.R +import com.android.systemui.controls.settings.ControlsSettingsDialogManager.Companion.MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG +import com.android.systemui.controls.settings.ControlsSettingsDialogManager.Companion.PREFS_SETTINGS_DIALOG_ATTEMPTS +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl +import com.android.systemui.util.settings.SecureSettings +import javax.inject.Inject + +/** + * Manager to display a dialog to prompt user to enable controls related Settings: + * + * * [Settings.Secure.LOCKSCREEN_SHOW_CONTROLS] + * * [Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS] + */ +interface ControlsSettingsDialogManager { + + /** + * Shows the corresponding dialog. In order for a dialog to appear, the following must be true + * + * * At least one of the Settings in [ControlsSettingsRepository] are `false`. + * * The dialog has not been seen by the user too many times (as defined by + * [MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG]). + * + * When the dialogs are shown, the following outcomes are possible: + * * User cancels the dialog by clicking outside or going back: we register that the dialog was + * seen but the settings don't change. + * * User responds negatively to the dialog: we register that the user doesn't want to change + * the settings (dialog will not appear again) and the settings don't change. + * * User responds positively to the dialog: the settings are set to `true` and the dialog will + * not appear again. + * * SystemUI closes the dialogs (for example, the activity showing it is closed). In this case, + * we don't modify anything. + * + * Of those four scenarios, only the first three will cause [onAttemptCompleted] to be called. + * It will also be called if the dialogs are not shown. + */ + fun maybeShowDialog(activityContext: Context, onAttemptCompleted: () -> Unit) + + /** + * Closes the dialog without registering anything from the user. The state of the settings after + * this is called will be the same as before the dialogs were shown. + */ + fun closeDialog() + + companion object { + @VisibleForTesting internal const val MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG = 2 + @VisibleForTesting + internal const val PREFS_SETTINGS_DIALOG_ATTEMPTS = "show_settings_attempts" + } +} + +@SysUISingleton +class ControlsSettingsDialogManagerImpl +@VisibleForTesting +internal constructor( + private val secureSettings: SecureSettings, + private val userFileManager: UserFileManager, + private val controlsSettingsRepository: ControlsSettingsRepository, + private val userTracker: UserTracker, + private val activityStarter: ActivityStarter, + private val dialogProvider: (context: Context, theme: Int) -> AlertDialog +) : ControlsSettingsDialogManager { + + @Inject + constructor( + secureSettings: SecureSettings, + userFileManager: UserFileManager, + controlsSettingsRepository: ControlsSettingsRepository, + userTracker: UserTracker, + activityStarter: ActivityStarter + ) : this( + secureSettings, + userFileManager, + controlsSettingsRepository, + userTracker, + activityStarter, + { context, theme -> SettingsDialog(context, theme) } + ) + + private var dialog: AlertDialog? = null + private set + + private val showDeviceControlsInLockscreen: Boolean + get() = controlsSettingsRepository.canShowControlsInLockscreen.value + + private val allowTrivialControls: Boolean + get() = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value + + override fun maybeShowDialog(activityContext: Context, onAttemptCompleted: () -> Unit) { + closeDialog() + + val prefs = + userFileManager.getSharedPreferences( + DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, + MODE_PRIVATE, + userTracker.userId + ) + val attempts = prefs.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0) + if ( + attempts >= MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG || + (showDeviceControlsInLockscreen && allowTrivialControls) + ) { + onAttemptCompleted() + return + } + + val listener = DialogListener(prefs, attempts, onAttemptCompleted) + val d = + dialogProvider(activityContext, R.style.Theme_SystemUI_Dialog).apply { + setIcon(R.drawable.ic_warning) + setOnCancelListener(listener) + setNeutralButton(R.string.controls_settings_dialog_neutral_button, listener) + setPositiveButton(R.string.controls_settings_dialog_positive_button, listener) + if (showDeviceControlsInLockscreen) { + setTitle(R.string.controls_settings_trivial_controls_dialog_title) + setMessage(R.string.controls_settings_trivial_controls_dialog_message) + } else { + setTitle(R.string.controls_settings_show_controls_dialog_title) + setMessage(R.string.controls_settings_show_controls_dialog_message) + } + } + + SystemUIDialog.registerDismissListener(d) { dialog = null } + SystemUIDialog.setDialogSize(d) + SystemUIDialog.setShowForAllUsers(d, true) + dialog = d + d.show() + } + + private fun turnOnSettingSecurely(settings: List<String>) { + val action = + ActivityStarter.OnDismissAction { + settings.forEach { setting -> + secureSettings.putIntForUser(setting, 1, userTracker.userId) + } + true + } + activityStarter.dismissKeyguardThenExecute( + action, + /* cancel */ null, + /* afterKeyguardGone */ true + ) + } + + override fun closeDialog() { + dialog?.dismiss() + } + + private inner class DialogListener( + private val prefs: SharedPreferences, + private val attempts: Int, + private val onComplete: () -> Unit + ) : DialogInterface.OnClickListener, DialogInterface.OnCancelListener { + override fun onClick(dialog: DialogInterface?, which: Int) { + if (dialog == null) return + if (which == DialogInterface.BUTTON_POSITIVE) { + val settings = mutableListOf(Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS) + if (!showDeviceControlsInLockscreen) { + settings.add(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS) + } + turnOnSettingSecurely(settings) + } + if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { + prefs + .edit() + .putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) + .apply() + } + onComplete() + } + + override fun onCancel(dialog: DialogInterface?) { + if (dialog == null) return + if (attempts < MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { + prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, attempts + 1).apply() + } + onComplete() + } + } + + private fun AlertDialog.setNeutralButton( + msgId: Int, + listener: DialogInterface.OnClickListener + ) { + setButton(DialogInterface.BUTTON_NEUTRAL, context.getText(msgId), listener) + } + + private fun AlertDialog.setPositiveButton( + msgId: Int, + listener: DialogInterface.OnClickListener + ) { + setButton(DialogInterface.BUTTON_POSITIVE, context.getText(msgId), listener) + } + + private fun AlertDialog.setMessage(msgId: Int) { + setMessage(context.getText(msgId)) + } + + /** This is necessary because the constructors are `protected`. */ + private class SettingsDialog(context: Context, theme: Int) : AlertDialog(context, theme) +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepository.kt index 3d10ab906f2f..df2831c6eb96 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepository.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.controls +package com.android.systemui.controls.settings import kotlinx.coroutines.flow.StateFlow diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt index 9dc422a09674..8e3b5109339c 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ControlsSettingsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.controls +package com.android.systemui.controls.settings import android.provider.Settings import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt index 041ed1d557d7..99a10a33ab0f 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt @@ -19,15 +19,12 @@ package com.android.systemui.controls.ui import android.annotation.AnyThread import android.annotation.MainThread import android.app.Activity -import android.app.AlertDialog import android.app.Dialog import android.app.PendingIntent import android.content.Context import android.content.pm.PackageManager import android.content.pm.ResolveInfo -import android.os.UserHandle import android.os.VibrationEffect -import android.provider.Settings.Secure import android.service.controls.Control import android.service.controls.actions.BooleanAction import android.service.controls.actions.CommandAction @@ -35,39 +32,36 @@ import android.service.controls.actions.FloatAction import android.util.Log import android.view.HapticFeedbackConstants import com.android.internal.annotations.VisibleForTesting -import com.android.systemui.R import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.controls.ControlsMetricsLogger -import com.android.systemui.controls.ControlsSettingsRepository +import com.android.systemui.controls.settings.ControlsSettingsDialogManager +import com.android.systemui.controls.settings.ControlsSettingsRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.statusbar.phone.SystemUIDialog -import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl.Companion.PREFS_CONTROLS_FILE -import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl.Companion.PREFS_SETTINGS_DIALOG_ATTEMPTS import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.systemui.util.settings.SecureSettings import com.android.wm.shell.TaskViewFactory import java.util.Optional import javax.inject.Inject @SysUISingleton class ControlActionCoordinatorImpl @Inject constructor( - private val context: Context, - private val bgExecutor: DelayableExecutor, - @Main private val uiExecutor: DelayableExecutor, - private val activityStarter: ActivityStarter, - private val broadcastSender: BroadcastSender, - private val keyguardStateController: KeyguardStateController, - private val taskViewFactory: Optional<TaskViewFactory>, - private val controlsMetricsLogger: ControlsMetricsLogger, - private val vibrator: VibratorHelper, - private val secureSettings: SecureSettings, - private val userContextProvider: UserContextProvider, - private val controlsSettingsRepository: ControlsSettingsRepository, + private val context: Context, + private val bgExecutor: DelayableExecutor, + @Main private val uiExecutor: DelayableExecutor, + private val activityStarter: ActivityStarter, + private val broadcastSender: BroadcastSender, + private val keyguardStateController: KeyguardStateController, + private val taskViewFactory: Optional<TaskViewFactory>, + private val controlsMetricsLogger: ControlsMetricsLogger, + private val vibrator: VibratorHelper, + private val controlsSettingsRepository: ControlsSettingsRepository, + private val controlsSettingsDialogManager: ControlsSettingsDialogManager, + private val featureFlags: FeatureFlags, ) : ControlActionCoordinator { private var dialog: Dialog? = null private var pendingAction: Action? = null @@ -76,16 +70,16 @@ class ControlActionCoordinatorImpl @Inject constructor( get() = !keyguardStateController.isUnlocked() private val allowTrivialControls: Boolean get() = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value - private val showDeviceControlsInLockscreen: Boolean - get() = controlsSettingsRepository.canShowControlsInLockscreen.value override lateinit var activityContext: Context companion object { private const val RESPONSE_TIMEOUT_IN_MILLIS = 3000L - private const val MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG = 2 } override fun closeDialogs() { + if (!featureFlags.isEnabled(Flags.USE_APP_PANELS)) { + controlsSettingsDialogManager.closeDialog() + } val isActivityFinishing = (activityContext as? Activity)?.let { it.isFinishing || it.isDestroyed } if (isActivityFinishing == true) { @@ -253,71 +247,9 @@ class ControlActionCoordinatorImpl @Inject constructor( if (action.authIsRequired) { return } - val prefs = userContextProvider.userContext.getSharedPreferences( - PREFS_CONTROLS_FILE, Context.MODE_PRIVATE) - val attempts = prefs.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0) - if (attempts >= MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG || - (showDeviceControlsInLockscreen && allowTrivialControls)) { - return + if (!featureFlags.isEnabled(Flags.USE_APP_PANELS)) { + controlsSettingsDialogManager.maybeShowDialog(activityContext) {} } - val builder = AlertDialog - .Builder(activityContext, R.style.Theme_SystemUI_Dialog) - .setIcon(R.drawable.ic_warning) - .setOnCancelListener { - if (attempts < MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { - prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, attempts + 1) - .commit() - } - true - } - .setNeutralButton(R.string.controls_settings_dialog_neutral_button) { _, _ -> - if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { - prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, - MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) - .commit() - } - true - } - - if (showDeviceControlsInLockscreen) { - dialog = builder - .setTitle(R.string.controls_settings_trivial_controls_dialog_title) - .setMessage(R.string.controls_settings_trivial_controls_dialog_message) - .setPositiveButton(R.string.controls_settings_dialog_positive_button) { _, _ -> - if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { - prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, - MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) - .commit() - } - secureSettings.putIntForUser(Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, 1, - UserHandle.USER_CURRENT) - true - } - .create() - } else { - dialog = builder - .setTitle(R.string.controls_settings_show_controls_dialog_title) - .setMessage(R.string.controls_settings_show_controls_dialog_message) - .setPositiveButton(R.string.controls_settings_dialog_positive_button) { _, _ -> - if (attempts != MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) { - prefs.edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, - MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) - .commit() - } - secureSettings.putIntForUser(Secure.LOCKSCREEN_SHOW_CONTROLS, - 1, UserHandle.USER_CURRENT) - secureSettings.putIntForUser(Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, - 1, UserHandle.USER_CURRENT) - true - } - .create() - } - - SystemUIDialog.registerDismissListener(dialog) - SystemUIDialog.setDialogSize(dialog) - - dialog?.create() - dialog?.show() } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt index bd704c1ff086..5d611c4c8212 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt @@ -32,8 +32,10 @@ import androidx.activity.ComponentActivity import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.management.ControlsAnimations +import com.android.systemui.controls.settings.ControlsSettingsDialogManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject /** @@ -47,7 +49,9 @@ class ControlsActivity @Inject constructor( private val uiController: ControlsUiController, private val broadcastDispatcher: BroadcastDispatcher, private val dreamManager: IDreamManager, - private val featureFlags: FeatureFlags + private val featureFlags: FeatureFlags, + private val controlsSettingsDialogManager: ControlsSettingsDialogManager, + private val keyguardStateController: KeyguardStateController ) : ComponentActivity() { private lateinit var parent: ViewGroup @@ -92,7 +96,13 @@ class ControlsActivity @Inject constructor( parent = requireViewById<ViewGroup>(R.id.global_actions_controls) parent.alpha = 0f - uiController.show(parent, { finishOrReturnToDream() }, this) + if (featureFlags.isEnabled(Flags.USE_APP_PANELS) && !keyguardStateController.isUnlocked) { + controlsSettingsDialogManager.maybeShowDialog(this) { + uiController.show(parent, { finishOrReturnToDream() }, this) + } + } else { + uiController.show(parent, { finishOrReturnToDream() }, this) + } ControlsAnimations.enterAnimation(parent).start() } @@ -124,6 +134,7 @@ class ControlsActivity @Inject constructor( mExitToDream = false uiController.hide() + controlsSettingsDialogManager.closeDialog() } override fun onDestroy() { diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index a07c716bc8f9..fb678aa420bf 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -49,7 +49,7 @@ import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsServiceInfo -import com.android.systemui.controls.ControlsSettingsRepository +import com.android.systemui.controls.settings.ControlsSettingsRepository import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 6ed555056cb1..306e92e6c96c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -2214,6 +2214,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, case START_KEYGUARD_EXIT_ANIM: Trace.beginSection( "KeyguardViewMediator#handleMessage START_KEYGUARD_EXIT_ANIM"); + synchronized (KeyguardViewMediator.this) { + mHiding = true; + } StartKeyguardExitAnimParams params = (StartKeyguardExitAnimParams) msg.obj; mNotificationShadeWindowControllerLazy.get().batchApplyWindowLayoutParams( () -> { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt index 6c66f0bb1e47..341eb3b0425c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt @@ -68,7 +68,6 @@ public class DeviceControlsControllerImpl @Inject constructor( internal const val PREFS_CONTROLS_SEEDING_COMPLETED = "SeedingCompleted" const val PREFS_CONTROLS_FILE = "controls_prefs" - internal const val PREFS_SETTINGS_DIALOG_ATTEMPTS = "show_settings_attempts" private const val SEEDING_MAX = 2 } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index b267a5c23a49..a94f3427eebe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -110,6 +110,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import javax.inject.Provider; + @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper(setAsMainLooper = true) @@ -261,6 +263,7 @@ public class UdfpsControllerTest extends SysuiTestCase { initUdfpsController(true /* hasAlternateTouchProvider */); } + private void initUdfpsController(boolean hasAlternateTouchProvider) { initUdfpsController(mOpticalProps, hasAlternateTouchProvider); } @@ -270,8 +273,10 @@ public class UdfpsControllerTest extends SysuiTestCase { reset(mFingerprintManager); reset(mScreenLifecycle); - final Optional<AlternateUdfpsTouchProvider> alternateTouchProvider = - hasAlternateTouchProvider ? Optional.of(mAlternateTouchProvider) : Optional.empty(); + final Optional<Provider<AlternateUdfpsTouchProvider>> alternateTouchProvider = + hasAlternateTouchProvider ? Optional.of( + (Provider<AlternateUdfpsTouchProvider>) () -> mAlternateTouchProvider) + : Optional.empty(); mUdfpsController = new UdfpsController(mContext, new FakeExecution(), mLayoutInflater, mFingerprintManager, mWindowManager, mStatusBarStateController, mFgExecutor, @@ -1140,7 +1145,7 @@ public class UdfpsControllerTest extends SysuiTestCase { } @Test - public void onTouch_withNewTouchDetection_shouldCallOldFingerprintManagerPath() + public void onTouch_withNewTouchDetection_shouldCallNewFingerprintManagerPath() throws RemoteException { final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L, 0L); diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt index 1d00d6b05568..16fb50c15af6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt @@ -16,21 +16,19 @@ package com.android.systemui.controls.ui -import android.content.Context -import android.content.SharedPreferences import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.controls.ControlsMetricsLogger -import com.android.systemui.controls.FakeControlsSettingsRepository +import com.android.systemui.controls.settings.ControlsSettingsDialogManager +import com.android.systemui.controls.settings.FakeControlsSettingsRepository +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.systemui.util.settings.SecureSettings import com.android.wm.shell.TaskViewFactory import org.junit.Before import org.junit.Test @@ -40,8 +38,8 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.`when` import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.doNothing import org.mockito.Mockito.doReturn -import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.spy @@ -71,9 +69,9 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { @Mock private lateinit var metricsLogger: ControlsMetricsLogger @Mock - private lateinit var secureSettings: SecureSettings + private lateinit var featureFlags: FeatureFlags @Mock - private lateinit var userContextProvider: UserContextProvider + private lateinit var controlsSettingsDialogManager: ControlsSettingsDialogManager companion object { fun <T> any(): T = Mockito.any<T>() @@ -103,23 +101,16 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { taskViewFactory, metricsLogger, vibratorHelper, - secureSettings, - userContextProvider, - controlsSettingsRepository + controlsSettingsRepository, + controlsSettingsDialogManager, + featureFlags )) - - val userContext = mock(Context::class.java) - val pref = mock(SharedPreferences::class.java) - `when`(userContextProvider.userContext).thenReturn(userContext) - `when`(userContext.getSharedPreferences( - DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, Context.MODE_PRIVATE)) - .thenReturn(pref) - // Just return 2 so we don't test any Dialog logic which requires a launched activity. - `when`(pref.getInt(DeviceControlsControllerImpl.PREFS_SETTINGS_DIALOG_ATTEMPTS, 0)) - .thenReturn(2) + coordinator.activityContext = mContext `when`(cvh.cws.ci.controlId).thenReturn(ID) `when`(cvh.cws.control?.isAuthRequired()).thenReturn(true) + `when`(featureFlags.isEnabled(Flags.USE_APP_PANELS)).thenReturn(false) + action = spy(coordinator.Action(ID, {}, false, true)) doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean(), anyBoolean()) } @@ -160,15 +151,31 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean(), anyBoolean()) `when`(keyguardStateController.isShowing()).thenReturn(true) - `when`(keyguardStateController.isUnlocked()).thenReturn(false) coordinator.toggle(cvh, "", true) verify(coordinator).bouncerOrRun(action) + verify(controlsSettingsDialogManager).maybeShowDialog(any(), any()) verify(action).invoke() } @Test + fun testToggleWhenLockedDoesNotTriggerDialog_featureFlagEnabled() { + `when`(featureFlags.isEnabled(Flags.USE_APP_PANELS)).thenReturn(true) + action = spy(coordinator.Action(ID, {}, false, false)) + doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean(), anyBoolean()) + + `when`(keyguardStateController.isShowing()).thenReturn(true) + `when`(keyguardStateController.isUnlocked()).thenReturn(false) + doNothing().`when`(controlsSettingsDialogManager).maybeShowDialog(any(), any()) + + coordinator.toggle(cvh, "", true) + + verify(coordinator).bouncerOrRun(action) + verify(controlsSettingsDialogManager, never()).maybeShowDialog(any(), any()) + } + + @Test fun testToggleDoesNotRunsWhenLockedAndAuthRequired() { action = spy(coordinator.Action(ID, {}, false, true)) doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean(), anyBoolean()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt index 48fc46b7e730..9144b13c7f3e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt @@ -22,7 +22,7 @@ import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT import com.android.systemui.SysuiTestCase -import com.android.systemui.controls.FakeControlsSettingsRepository +import com.android.systemui.controls.settings.FakeControlsSettingsRepository import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.ControlsTileResourceConfiguration import com.android.systemui.controls.management.ControlsListingController diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsDialogManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsDialogManagerImplTest.kt new file mode 100644 index 000000000000..0c9986d82447 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsDialogManagerImplTest.kt @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.controls.settings + +import android.content.DialogInterface +import android.content.SharedPreferences +import android.database.ContentObserver +import android.provider.Settings.Secure.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS +import android.provider.Settings.Secure.LOCKSCREEN_SHOW_CONTROLS +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.controls.settings.ControlsSettingsDialogManager.Companion.PREFS_SETTINGS_DIALOG_ATTEMPTS +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl +import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.TestableAlertDialog +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.nullable +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class ControlsSettingsDialogManagerImplTest : SysuiTestCase() { + + companion object { + private const val SETTING_SHOW = LOCKSCREEN_SHOW_CONTROLS + private const val SETTING_ACTION = LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS + private const val MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG = 2 + } + + @Mock private lateinit var userFileManager: UserFileManager + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var completedRunnable: () -> Unit + + private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository + private lateinit var sharedPreferences: FakeSharedPreferences + private lateinit var secureSettings: FakeSettings + + private lateinit var underTest: ControlsSettingsDialogManagerImpl + + private var dialog: TestableAlertDialog? = null + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + controlsSettingsRepository = FakeControlsSettingsRepository() + sharedPreferences = FakeSharedPreferences() + secureSettings = FakeSettings() + + `when`(userTracker.userId).thenReturn(0) + secureSettings.userId = userTracker.userId + `when`( + userFileManager.getSharedPreferences( + eq(DeviceControlsControllerImpl.PREFS_CONTROLS_FILE), + anyInt(), + anyInt() + ) + ) + .thenReturn(sharedPreferences) + + `when`(activityStarter.dismissKeyguardThenExecute(any(), nullable(), anyBoolean())) + .thenAnswer { (it.arguments[0] as ActivityStarter.OnDismissAction).onDismiss() } + + attachRepositoryToSettings() + underTest = + ControlsSettingsDialogManagerImpl( + secureSettings, + userFileManager, + controlsSettingsRepository, + userTracker, + activityStarter + ) { context, _ -> TestableAlertDialog(context).also { dialog = it } } + } + + @After + fun tearDown() { + underTest.closeDialog() + } + + @Test + fun dialogNotShownIfPrefsAtMaximum() { + sharedPreferences.putAttempts(MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) + + underTest.maybeShowDialog(context, completedRunnable) + + assertThat(dialog?.isShowing ?: false).isFalse() + verify(completedRunnable).invoke() + } + + @Test + fun dialogNotShownIfSettingsAreTrue() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, true) + + underTest.maybeShowDialog(context, completedRunnable) + + assertThat(dialog?.isShowing ?: false).isFalse() + verify(completedRunnable).invoke() + } + + @Test + fun dialogShownIfAllowTrivialControlsFalse() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + + assertThat(dialog?.isShowing ?: false).isTrue() + } + + @Test + fun dialogDispossedAfterClosing() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + underTest.closeDialog() + + assertThat(dialog?.isShowing ?: false).isFalse() + } + + @Test + fun dialogNeutralButtonDoesntChangeSetting() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + clickButton(DialogInterface.BUTTON_NEUTRAL) + + assertThat(secureSettings.getBool(SETTING_ACTION, false)).isFalse() + } + + @Test + fun dialogNeutralButtonPutsMaxAttempts() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + clickButton(DialogInterface.BUTTON_NEUTRAL) + + assertThat(sharedPreferences.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0)) + .isEqualTo(MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) + } + + @Test + fun dialogNeutralButtonCallsOnComplete() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + clickButton(DialogInterface.BUTTON_NEUTRAL) + + verify(completedRunnable).invoke() + } + + @Test + fun dialogPositiveButtonChangesSetting() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + clickButton(DialogInterface.BUTTON_POSITIVE) + + assertThat(secureSettings.getBool(SETTING_ACTION, false)).isTrue() + } + + @Test + fun dialogPositiveButtonPutsMaxAttempts() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + clickButton(DialogInterface.BUTTON_POSITIVE) + + assertThat(sharedPreferences.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0)) + .isEqualTo(MAX_NUMBER_ATTEMPTS_CONTROLS_DIALOG) + } + + @Test + fun dialogPositiveButtonCallsOnComplete() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + clickButton(DialogInterface.BUTTON_POSITIVE) + + verify(completedRunnable).invoke() + } + + @Test + fun dialogCancelDoesntChangeSetting() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + dialog?.cancel() + + assertThat(secureSettings.getBool(SETTING_ACTION, false)).isFalse() + } + + @Test + fun dialogCancelPutsOneExtraAttempt() { + val attempts = 0 + sharedPreferences.putAttempts(attempts) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + dialog?.cancel() + + assertThat(sharedPreferences.getInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, 0)) + .isEqualTo(attempts + 1) + } + + @Test + fun dialogCancelCallsOnComplete() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + dialog?.cancel() + + verify(completedRunnable).invoke() + } + + @Test + fun closeDialogDoesNotCallOnComplete() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, true) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + underTest.closeDialog() + + verify(completedRunnable, never()).invoke() + } + + @Test + fun dialogPositiveWithBothSettingsFalseTogglesBothSettings() { + sharedPreferences.putAttempts(0) + secureSettings.putBool(SETTING_SHOW, false) + secureSettings.putBool(SETTING_ACTION, false) + + underTest.maybeShowDialog(context, completedRunnable) + clickButton(DialogInterface.BUTTON_POSITIVE) + + assertThat(secureSettings.getBool(SETTING_SHOW)).isTrue() + assertThat(secureSettings.getBool(SETTING_ACTION)).isTrue() + } + + private fun clickButton(which: Int) { + dialog?.clickButton(which) + } + + private fun attachRepositoryToSettings() { + secureSettings.registerContentObserver( + SETTING_SHOW, + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + controlsSettingsRepository.setCanShowControlsInLockscreen( + secureSettings.getBool(SETTING_SHOW, false) + ) + } + } + ) + + secureSettings.registerContentObserver( + SETTING_ACTION, + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen( + secureSettings.getBool(SETTING_ACTION, false) + ) + } + } + ) + } + + private fun SharedPreferences.putAttempts(value: Int) { + edit().putInt(PREFS_SETTINGS_DIALOG_ATTEMPTS, value).commit() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ControlsSettingsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImplTest.kt index 4b88b44c3f03..b904ac14e707 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ControlsSettingsRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImplTest.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.controls +package com.android.systemui.controls.settings import android.content.pm.UserInfo import android.provider.Settings diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/FakeControlsSettingsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt index 8a1bed20e700..b6628db14235 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/FakeControlsSettingsRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.controls +package com.android.systemui.controls.settings import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index d965e337f47a..779788aa0075 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -35,10 +35,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.CustomIconCache -import com.android.systemui.controls.FakeControlsSettingsRepository import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.settings.FakeControlsSettingsRepository import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index d17e3744edc6..798839dcc1f6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -16,6 +16,7 @@ package com.android.systemui.keyguard; +import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY; import static android.view.WindowManagerPolicyConstants.OFF_BECAUSE_OF_USER; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW; @@ -34,6 +35,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.IActivityManager; import android.app.admin.DevicePolicyManager; import android.app.trust.TrustManager; import android.os.PowerManager; @@ -41,6 +43,11 @@ import android.os.PowerManager.WakeLock; import android.telephony.TelephonyManager; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.RemoteAnimationTarget; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.WindowManager; import androidx.test.filters.SmallTest; @@ -52,21 +59,27 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.mediator.ScreenOnCoordinator; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.biometrics.AuthController; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.classifier.FalsingCollectorFake; +import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dump.DumpManager; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.settings.UserTracker; +import com.android.systemui.shade.NotificationShadeWindowControllerImpl; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.DozeParameters; +import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; +import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.util.DeviceConfigProxy; @@ -80,8 +93,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import dagger.Lazy; - @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @SmallTest @@ -96,11 +107,15 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private @Mock BroadcastDispatcher mBroadcastDispatcher; private @Mock DismissCallbackRegistry mDismissCallbackRegistry; private @Mock DumpManager mDumpManager; + private @Mock WindowManager mWindowManager; + private @Mock IActivityManager mActivityManager; + private @Mock ConfigurationController mConfigurationController; private @Mock PowerManager mPowerManager; private @Mock TrustManager mTrustManager; private @Mock UserSwitcherController mUserSwitcherController; private @Mock NavigationModeController mNavigationModeController; private @Mock KeyguardDisplayManager mKeyguardDisplayManager; + private @Mock KeyguardBypassController mKeyguardBypassController; private @Mock DozeParameters mDozeParameters; private @Mock SysuiStatusBarStateController mStatusBarStateController; private @Mock KeyguardStateController mKeyguardStateController; @@ -110,10 +125,13 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private @Mock InteractionJankMonitor mInteractionJankMonitor; private @Mock ScreenOnCoordinator mScreenOnCoordinator; private @Mock ShadeController mShadeController; - private @Mock Lazy<NotificationShadeWindowController> mNotificationShadeWindowControllerLazy; + private NotificationShadeWindowController mNotificationShadeWindowController; private @Mock DreamOverlayStateController mDreamOverlayStateController; private @Mock ActivityLaunchAnimator mActivityLaunchAnimator; private @Mock ScrimController mScrimController; + private @Mock SysuiColorExtractor mColorExtractor; + private @Mock AuthController mAuthController; + private @Mock ShadeExpansionStateManager mShadeExpansionStateManager; private DeviceConfigProxy mDeviceConfig = new DeviceConfigProxyFake(); private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock()); @@ -130,6 +148,14 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { when(mPowerManager.newWakeLock(anyInt(), any())).thenReturn(mock(WakeLock.class)); when(mInteractionJankMonitor.begin(any(), anyInt())).thenReturn(true); when(mInteractionJankMonitor.end(anyInt())).thenReturn(true); + final ViewRootImpl testViewRoot = mock(ViewRootImpl.class); + when(testViewRoot.getView()).thenReturn(mock(View.class)); + when(mStatusBarKeyguardViewManager.getViewRootImpl()).thenReturn(testViewRoot); + mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl(mContext, + mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController, + mConfigurationController, mViewMediator, mKeyguardBypassController, + mColorExtractor, mDumpManager, mKeyguardStateController, + mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager); createAndStartViewMediator(); } @@ -287,6 +313,23 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { verify(mCentralSurfaces).updateIsKeyguard(); } + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testStartKeyguardExitAnimation_expectSurfaceBehindRemoteAnimation() { + RemoteAnimationTarget[] apps = new RemoteAnimationTarget[]{ + mock(RemoteAnimationTarget.class) + }; + RemoteAnimationTarget[] wallpapers = new RemoteAnimationTarget[]{ + mock(RemoteAnimationTarget.class) + }; + IRemoteAnimationFinishedCallback callback = mock(IRemoteAnimationFinishedCallback.class); + + mViewMediator.startKeyguardExitAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, apps, wallpapers, + null, callback); + TestableLooper.get(this).processAllMessages(); + assertTrue(mViewMediator.isAnimatingBetweenKeyguardAndSurfaceBehind()); + } + private void createAndStartViewMediator() { mViewMediator = new KeyguardViewMediator( mContext, @@ -315,7 +358,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mInteractionJankMonitor, mDreamOverlayStateController, () -> mShadeController, - mNotificationShadeWindowControllerLazy, + () -> mNotificationShadeWindowController, () -> mActivityLaunchAnimator, () -> mScrimController); mViewMediator.start(); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index dc1d50c435b9..cd777092d438 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -1891,7 +1891,18 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub // actions. taskFragment.setTaskFragmentOrganizer(creationParams.getOrganizer(), ownerActivity.getUid(), ownerActivity.info.processName); - ownerTask.addChild(taskFragment, POSITION_TOP); + final int position; + if (creationParams.getPairedPrimaryFragmentToken() != null) { + // When there is a paired primary TaskFragment, we want to place the new TaskFragment + // right above the paired one to make sure there is no other window in between. + final TaskFragment pairedPrimaryTaskFragment = getTaskFragment( + creationParams.getPairedPrimaryFragmentToken()); + final int pairedPosition = ownerTask.mChildren.indexOf(pairedPrimaryTaskFragment); + position = pairedPosition != -1 ? pairedPosition + 1 : POSITION_TOP; + } else { + position = POSITION_TOP; + } + ownerTask.addChild(taskFragment, position); taskFragment.setWindowingMode(creationParams.getWindowingMode()); taskFragment.setBounds(creationParams.getInitialBounds()); mLaunchTaskFragments.put(creationParams.getFragmentToken(), taskFragment); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 2b493145f854..6cb3450ee470 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -552,10 +552,9 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { @Test public void testApplyTransaction_enforceHierarchyChange_createTaskFragment() { final ActivityRecord ownerActivity = createActivityRecord(mDisplayContent); - final IBinder fragmentToken = new Binder(); // Allow organizer to create TaskFragment and start/reparent activity to TaskFragment. - createTaskFragmentFromOrganizer(mTransaction, ownerActivity, fragmentToken); + createTaskFragmentFromOrganizer(mTransaction, ownerActivity, mFragmentToken); mTransaction.startActivityInTaskFragment( mFragmentToken, null /* callerToken */, new Intent(), null /* activityOptions */); mTransaction.reparentActivityToTaskFragment(mFragmentToken, mock(IBinder.class)); @@ -564,7 +563,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { assertApplyTransactionAllowed(mTransaction); // Successfully created a TaskFragment. - final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment(fragmentToken); + final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment( + mFragmentToken); assertNotNull(taskFragment); assertEquals(ownerActivity.getTask(), taskFragment.getTask()); } @@ -703,6 +703,40 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test + public void testApplyTransaction_createTaskFragment_withPairedPrimaryFragmentToken() { + final Task task = createTask(mDisplayContent); + mTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(mFragmentToken) + .createActivityCount(1) + .build(); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + final ActivityRecord activityOnTop = createActivityRecord(task); + final int uid = Binder.getCallingUid(); + activityOnTop.info.applicationInfo.uid = uid; + activityOnTop.getTask().effectiveUid = uid; + final IBinder fragmentToken1 = new Binder(); + final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder( + mOrganizerToken, fragmentToken1, activityOnTop.token) + .setPairedPrimaryFragmentToken(mFragmentToken) + .build(); + mTransaction.setTaskFragmentOrganizer(mIOrganizer); + mTransaction.createTaskFragment(params); + assertApplyTransactionAllowed(mTransaction); + + // Successfully created a TaskFragment. + final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment( + fragmentToken1); + assertNotNull(taskFragment); + // The new TaskFragment should be positioned right above the paired TaskFragment. + assertEquals(task.mChildren.indexOf(mTaskFragment) + 1, + task.mChildren.indexOf(taskFragment)); + // The top activity should remain on top. + assertEquals(task.mChildren.indexOf(taskFragment) + 1, + task.mChildren.indexOf(activityOnTop)); + } + + @Test public void testApplyTransaction_enforceHierarchyChange_reparentChildren() { doReturn(true).when(mTaskFragment).isAttached(); |