Merge "Adding supportedModes to DisplayDeviceConfig SensorData" into main
diff --git a/INPUT_OWNERS b/INPUT_OWNERS
index e02ba77..44b2f38 100644
--- a/INPUT_OWNERS
+++ b/INPUT_OWNERS
@@ -5,3 +5,5 @@
prabirmsp@google.com
svv@google.com
vdevmurari@google.com
+
+per-file Virtual*=file:/services/companion/java/com/android/server/companion/virtual/OWNERS
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index f6a7d2a..da8277c 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -271,7 +271,7 @@
final IBinder token = new Binder(
"android.hardware.input.VirtualDpad:" + config.getInputDeviceName());
mVirtualDevice.createVirtualDpad(config, token);
- return new VirtualDpad(mVirtualDevice, token);
+ return new VirtualDpad(config, mVirtualDevice, token);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -283,7 +283,7 @@
final IBinder token = new Binder(
"android.hardware.input.VirtualKeyboard:" + config.getInputDeviceName());
mVirtualDevice.createVirtualKeyboard(config, token);
- return new VirtualKeyboard(mVirtualDevice, token);
+ return new VirtualKeyboard(config, mVirtualDevice, token);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -295,7 +295,7 @@
final IBinder token = new Binder(
"android.hardware.input.VirtualMouse:" + config.getInputDeviceName());
mVirtualDevice.createVirtualMouse(config, token);
- return new VirtualMouse(mVirtualDevice, token);
+ return new VirtualMouse(config, mVirtualDevice, token);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -308,7 +308,7 @@
final IBinder token = new Binder(
"android.hardware.input.VirtualTouchscreen:" + config.getInputDeviceName());
mVirtualDevice.createVirtualTouchscreen(config, token);
- return new VirtualTouchscreen(mVirtualDevice, token);
+ return new VirtualTouchscreen(config, mVirtualDevice, token);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -322,7 +322,7 @@
"android.hardware.input.VirtualNavigationTouchpad:"
+ config.getInputDeviceName());
mVirtualDevice.createVirtualNavigationTouchpad(config, token);
- return new VirtualNavigationTouchpad(mVirtualDevice, token);
+ return new VirtualNavigationTouchpad(config, mVirtualDevice, token);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index 884d463..f532c4c 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -62,6 +62,8 @@
"mediaSharedWithParent";
private static final String ATTR_CREDENTIAL_SHAREABLE_WITH_PARENT =
"credentialShareableWithParent";
+ private static final String ATTR_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE =
+ "authAlwaysRequiredToDisableQuietMode";
private static final String ATTR_DELETE_APP_WITH_PARENT = "deleteAppWithParent";
private static final String ATTR_ALWAYS_VISIBLE = "alwaysVisible";
@@ -80,6 +82,7 @@
INDEX_DELETE_APP_WITH_PARENT,
INDEX_ALWAYS_VISIBLE,
INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE,
+ INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE,
})
@Retention(RetentionPolicy.SOURCE)
private @interface PropertyIndex {
@@ -97,6 +100,7 @@
private static final int INDEX_DELETE_APP_WITH_PARENT = 10;
private static final int INDEX_ALWAYS_VISIBLE = 11;
private static final int INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE = 12;
+ private static final int INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE = 13;
/** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */
private long mPropertiesPresent = 0;
@@ -329,6 +333,8 @@
setShowInSettings(orig.getShowInSettings());
setHideInSettingsInQuietMode(orig.getHideInSettingsInQuietMode());
setUseParentsContacts(orig.getUseParentsContacts());
+ setAuthAlwaysRequiredToDisableQuietMode(
+ orig.isAuthAlwaysRequiredToDisableQuietMode());
}
if (hasQueryOrManagePermission) {
// Add items that require QUERY_USERS or stronger.
@@ -611,6 +617,31 @@
}
private boolean mCredentialShareableWithParent;
+ /**
+ * Returns whether the profile always requires user authentication to disable from quiet mode.
+ *
+ * <p> Settings this field to true will ensure that the credential confirmation activity is
+ * always shown whenever the user requests to disable quiet mode. The behavior of credential
+ * checks is not guaranteed when the property is false and may vary depending on user types.
+ * @hide
+ */
+ public boolean isAuthAlwaysRequiredToDisableQuietMode() {
+ if (isPresent(INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE)) {
+ return mAuthAlwaysRequiredToDisableQuietMode;
+ }
+ if (mDefaultProperties != null) {
+ return mDefaultProperties.mAuthAlwaysRequiredToDisableQuietMode;
+ }
+ throw new SecurityException(
+ "You don't have permission to query authAlwaysRequiredToDisableQuietMode");
+ }
+ /** @hide */
+ public void setAuthAlwaysRequiredToDisableQuietMode(boolean val) {
+ this.mAuthAlwaysRequiredToDisableQuietMode = val;
+ setPresent(INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE);
+ }
+ private boolean mAuthAlwaysRequiredToDisableQuietMode;
+
/*
Indicate if {@link com.android.server.pm.CrossProfileIntentFilter}s need to be updated during
OTA update between user-parent
@@ -693,6 +724,8 @@
+ getCrossProfileIntentResolutionStrategy()
+ ", mMediaSharedWithParent=" + isMediaSharedWithParent()
+ ", mCredentialShareableWithParent=" + isCredentialShareableWithParent()
+ + ", mAuthAlwaysRequiredToDisableQuietMode="
+ + isAuthAlwaysRequiredToDisableQuietMode()
+ ", mDeleteAppWithParent=" + getDeleteAppWithParent()
+ ", mAlwaysVisible=" + getAlwaysVisible()
+ "}";
@@ -720,6 +753,8 @@
pw.println(prefix + " mMediaSharedWithParent=" + isMediaSharedWithParent());
pw.println(prefix + " mCredentialShareableWithParent="
+ isCredentialShareableWithParent());
+ pw.println(prefix + " mAuthAlwaysRequiredToDisableQuietMode="
+ + isAuthAlwaysRequiredToDisableQuietMode());
pw.println(prefix + " mDeleteAppWithParent=" + getDeleteAppWithParent());
pw.println(prefix + " mAlwaysVisible=" + getAlwaysVisible());
}
@@ -788,6 +823,9 @@
case ATTR_CREDENTIAL_SHAREABLE_WITH_PARENT:
setCredentialShareableWithParent(parser.getAttributeBoolean(i));
break;
+ case ATTR_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE:
+ setAuthAlwaysRequiredToDisableQuietMode(parser.getAttributeBoolean(i));
+ break;
case ATTR_DELETE_APP_WITH_PARENT:
setDeleteAppWithParent(parser.getAttributeBoolean(i));
break;
@@ -853,6 +891,10 @@
serializer.attributeBoolean(null, ATTR_CREDENTIAL_SHAREABLE_WITH_PARENT,
mCredentialShareableWithParent);
}
+ if (isPresent(INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE)) {
+ serializer.attributeBoolean(null, ATTR_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE,
+ mAuthAlwaysRequiredToDisableQuietMode);
+ }
if (isPresent(INDEX_DELETE_APP_WITH_PARENT)) {
serializer.attributeBoolean(null, ATTR_DELETE_APP_WITH_PARENT,
mDeleteAppWithParent);
@@ -878,6 +920,7 @@
dest.writeInt(mCrossProfileIntentResolutionStrategy);
dest.writeBoolean(mMediaSharedWithParent);
dest.writeBoolean(mCredentialShareableWithParent);
+ dest.writeBoolean(mAuthAlwaysRequiredToDisableQuietMode);
dest.writeBoolean(mDeleteAppWithParent);
dest.writeBoolean(mAlwaysVisible);
}
@@ -901,6 +944,7 @@
mCrossProfileIntentResolutionStrategy = source.readInt();
mMediaSharedWithParent = source.readBoolean();
mCredentialShareableWithParent = source.readBoolean();
+ mAuthAlwaysRequiredToDisableQuietMode = source.readBoolean();
mDeleteAppWithParent = source.readBoolean();
mAlwaysVisible = source.readBoolean();
}
@@ -941,6 +985,7 @@
CROSS_PROFILE_INTENT_RESOLUTION_STRATEGY_DEFAULT;
private boolean mMediaSharedWithParent = false;
private boolean mCredentialShareableWithParent = false;
+ private boolean mAuthAlwaysRequiredToDisableQuietMode = false;
private boolean mDeleteAppWithParent = false;
private boolean mAlwaysVisible = false;
@@ -1010,6 +1055,14 @@
return this;
}
+ /** Sets the value for {@link #mAuthAlwaysRequiredToDisableQuietMode} */
+ public Builder setAuthAlwaysRequiredToDisableQuietMode(
+ boolean authAlwaysRequiredToDisableQuietMode) {
+ mAuthAlwaysRequiredToDisableQuietMode =
+ authAlwaysRequiredToDisableQuietMode;
+ return this;
+ }
+
/** Sets the value for {@link #mDeleteAppWithParent}*/
public Builder setDeleteAppWithParent(boolean deleteAppWithParent) {
mDeleteAppWithParent = deleteAppWithParent;
@@ -1036,6 +1089,7 @@
mCrossProfileIntentResolutionStrategy,
mMediaSharedWithParent,
mCredentialShareableWithParent,
+ mAuthAlwaysRequiredToDisableQuietMode,
mDeleteAppWithParent,
mAlwaysVisible);
}
@@ -1053,6 +1107,7 @@
@CrossProfileIntentResolutionStrategy int crossProfileIntentResolutionStrategy,
boolean mediaSharedWithParent,
boolean credentialShareableWithParent,
+ boolean authAlwaysRequiredToDisableQuietMode,
boolean deleteAppWithParent,
boolean alwaysVisible) {
mDefaultProperties = null;
@@ -1067,6 +1122,8 @@
setCrossProfileIntentResolutionStrategy(crossProfileIntentResolutionStrategy);
setMediaSharedWithParent(mediaSharedWithParent);
setCredentialShareableWithParent(credentialShareableWithParent);
+ setAuthAlwaysRequiredToDisableQuietMode(
+ authAlwaysRequiredToDisableQuietMode);
setDeleteAppWithParent(deleteAppWithParent);
setAlwaysVisible(alwaysVisible);
}
diff --git a/core/java/android/hardware/input/VirtualDpad.java b/core/java/android/hardware/input/VirtualDpad.java
index 8133472..7f2d8a0 100644
--- a/core/java/android/hardware/input/VirtualDpad.java
+++ b/core/java/android/hardware/input/VirtualDpad.java
@@ -52,8 +52,8 @@
KeyEvent.KEYCODE_DPAD_CENTER)));
/** @hide */
- public VirtualDpad(IVirtualDevice virtualDevice, IBinder token) {
- super(virtualDevice, token);
+ public VirtualDpad(VirtualDpadConfig config, IVirtualDevice virtualDevice, IBinder token) {
+ super(config, virtualDevice, token);
}
/**
diff --git a/core/java/android/hardware/input/VirtualInputDevice.java b/core/java/android/hardware/input/VirtualInputDevice.java
index 772ba8e..931e1ff 100644
--- a/core/java/android/hardware/input/VirtualInputDevice.java
+++ b/core/java/android/hardware/input/VirtualInputDevice.java
@@ -42,9 +42,12 @@
*/
protected final IBinder mToken;
+ protected final VirtualInputDeviceConfig mConfig;
+
/** @hide */
- VirtualInputDevice(
+ VirtualInputDevice(VirtualInputDeviceConfig config,
IVirtualDevice virtualDevice, IBinder token) {
+ mConfig = config;
mVirtualDevice = virtualDevice;
mToken = token;
}
@@ -70,4 +73,9 @@
throw e.rethrowFromSystemServer();
}
}
+
+ @Override
+ public String toString() {
+ return mConfig.toString();
+ }
}
diff --git a/core/java/android/hardware/input/VirtualInputDeviceConfig.java b/core/java/android/hardware/input/VirtualInputDeviceConfig.java
index d3dacc9..a8caa58 100644
--- a/core/java/android/hardware/input/VirtualInputDeviceConfig.java
+++ b/core/java/android/hardware/input/VirtualInputDeviceConfig.java
@@ -91,6 +91,22 @@
dest.writeString8(mInputDeviceName);
}
+ @Override
+ public String toString() {
+ return getClass().getName() + "( "
+ + " name=" + mInputDeviceName
+ + " vendorId=" + mVendorId
+ + " productId=" + mProductId
+ + " associatedDisplayId=" + mAssociatedDisplayId
+ + additionalFieldsToString() + ")";
+ }
+
+ /** @hide */
+ @NonNull
+ String additionalFieldsToString() {
+ return "";
+ }
+
/**
* A builder for {@link VirtualInputDeviceConfig}
*
diff --git a/core/java/android/hardware/input/VirtualKeyEvent.java b/core/java/android/hardware/input/VirtualKeyEvent.java
index dc47f08..4d89508 100644
--- a/core/java/android/hardware/input/VirtualKeyEvent.java
+++ b/core/java/android/hardware/input/VirtualKeyEvent.java
@@ -205,6 +205,14 @@
return 0;
}
+ @Override
+ public String toString() {
+ return "VirtualKeyEvent("
+ + " action=" + KeyEvent.actionToString(mAction)
+ + " keyCode=" + KeyEvent.keyCodeToString(mKeyCode)
+ + " eventTime(ns)=" + mEventTimeNanos;
+ }
+
/**
* Returns the key code associated with this event.
*/
diff --git a/core/java/android/hardware/input/VirtualKeyboard.java b/core/java/android/hardware/input/VirtualKeyboard.java
index e569dbf..c90f893 100644
--- a/core/java/android/hardware/input/VirtualKeyboard.java
+++ b/core/java/android/hardware/input/VirtualKeyboard.java
@@ -39,8 +39,9 @@
private final int mUnsupportedKeyCode = KeyEvent.KEYCODE_DPAD_CENTER;
/** @hide */
- public VirtualKeyboard(IVirtualDevice virtualDevice, IBinder token) {
- super(virtualDevice, token);
+ public VirtualKeyboard(VirtualKeyboardConfig config,
+ IVirtualDevice virtualDevice, IBinder token) {
+ super(config, virtualDevice, token);
}
/**
diff --git a/core/java/android/hardware/input/VirtualKeyboardConfig.java b/core/java/android/hardware/input/VirtualKeyboardConfig.java
index 6d03065..96a1a36 100644
--- a/core/java/android/hardware/input/VirtualKeyboardConfig.java
+++ b/core/java/android/hardware/input/VirtualKeyboardConfig.java
@@ -99,6 +99,12 @@
dest.writeString8(mLayoutType);
}
+ @Override
+ @NonNull
+ String additionalFieldsToString() {
+ return " languageTag=" + mLanguageTag + " layoutType=" + mLayoutType;
+ }
+
/**
* Builder for creating a {@link VirtualKeyboardConfig}.
*/
diff --git a/core/java/android/hardware/input/VirtualMouse.java b/core/java/android/hardware/input/VirtualMouse.java
index 7eba2b8..51f3f69 100644
--- a/core/java/android/hardware/input/VirtualMouse.java
+++ b/core/java/android/hardware/input/VirtualMouse.java
@@ -38,8 +38,8 @@
public class VirtualMouse extends VirtualInputDevice {
/** @hide */
- public VirtualMouse(IVirtualDevice virtualDevice, IBinder token) {
- super(virtualDevice, token);
+ public VirtualMouse(VirtualMouseConfig config, IVirtualDevice virtualDevice, IBinder token) {
+ super(config, virtualDevice, token);
}
/**
diff --git a/core/java/android/hardware/input/VirtualMouseButtonEvent.java b/core/java/android/hardware/input/VirtualMouseButtonEvent.java
index dfdd3b4..fc42b15 100644
--- a/core/java/android/hardware/input/VirtualMouseButtonEvent.java
+++ b/core/java/android/hardware/input/VirtualMouseButtonEvent.java
@@ -110,6 +110,14 @@
return 0;
}
+ @Override
+ public String toString() {
+ return "VirtualMouseButtonEvent("
+ + " action=" + MotionEvent.actionToString(mAction)
+ + " button=" + MotionEvent.buttonStateToString(mButtonCode)
+ + " eventTime(ns)=" + mEventTimeNanos;
+ }
+
/**
* Returns the button code associated with this event.
*/
diff --git a/core/java/android/hardware/input/VirtualMouseRelativeEvent.java b/core/java/android/hardware/input/VirtualMouseRelativeEvent.java
index e6ad118..2a42cfc 100644
--- a/core/java/android/hardware/input/VirtualMouseRelativeEvent.java
+++ b/core/java/android/hardware/input/VirtualMouseRelativeEvent.java
@@ -61,6 +61,14 @@
return 0;
}
+ @Override
+ public String toString() {
+ return "VirtualMouseRelativeEvent("
+ + " x=" + mRelativeX
+ + " y=" + mRelativeY
+ + " eventTime(ns)=" + mEventTimeNanos;
+ }
+
/**
* Returns the relative x-axis movement, in pixels.
*/
diff --git a/core/java/android/hardware/input/VirtualMouseScrollEvent.java b/core/java/android/hardware/input/VirtualMouseScrollEvent.java
index 4d0a157..c89c188 100644
--- a/core/java/android/hardware/input/VirtualMouseScrollEvent.java
+++ b/core/java/android/hardware/input/VirtualMouseScrollEvent.java
@@ -65,6 +65,14 @@
return 0;
}
+ @Override
+ public String toString() {
+ return "VirtualMouseScrollEvent("
+ + " x=" + mXAxisMovement
+ + " y=" + mYAxisMovement
+ + " eventTime(ns)=" + mEventTimeNanos;
+ }
+
/**
* Returns the x-axis scroll movement, normalized from -1.0 to 1.0, inclusive. Positive values
* indicate scrolling upward; negative values, downward.
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpad.java b/core/java/android/hardware/input/VirtualNavigationTouchpad.java
index 2854034..61d72e2 100644
--- a/core/java/android/hardware/input/VirtualNavigationTouchpad.java
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpad.java
@@ -40,8 +40,9 @@
public class VirtualNavigationTouchpad extends VirtualInputDevice {
/** @hide */
- public VirtualNavigationTouchpad(IVirtualDevice virtualDevice, IBinder token) {
- super(virtualDevice, token);
+ public VirtualNavigationTouchpad(VirtualNavigationTouchpadConfig config,
+ IVirtualDevice virtualDevice, IBinder token) {
+ super(config, virtualDevice, token);
}
/**
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
index 8935efa..75f7b3e 100644
--- a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
@@ -70,6 +70,12 @@
dest.writeInt(mWidth);
}
+ @Override
+ @NonNull
+ String additionalFieldsToString() {
+ return " width=" + mWidth + " height=" + mHeight;
+ }
+
@NonNull
public static final Creator<VirtualNavigationTouchpadConfig> CREATOR =
new Creator<VirtualNavigationTouchpadConfig>() {
diff --git a/core/java/android/hardware/input/VirtualTouchEvent.java b/core/java/android/hardware/input/VirtualTouchEvent.java
index 2695a79..7936dfe 100644
--- a/core/java/android/hardware/input/VirtualTouchEvent.java
+++ b/core/java/android/hardware/input/VirtualTouchEvent.java
@@ -138,6 +138,19 @@
return 0;
}
+ @Override
+ public String toString() {
+ return "VirtualTouchEvent("
+ + " pointerId=" + mPointerId
+ + " toolType=" + MotionEvent.toolTypeToString(mToolType)
+ + " action=" + MotionEvent.actionToString(mAction)
+ + " x=" + mX
+ + " y=" + mY
+ + " pressure=" + mPressure
+ + " majorAxisSize=" + mMajorAxisSize
+ + " eventTime(ns)=" + mEventTimeNanos;
+ }
+
/**
* Returns the pointer id associated with this event.
*/
diff --git a/core/java/android/hardware/input/VirtualTouchscreen.java b/core/java/android/hardware/input/VirtualTouchscreen.java
index 0d07753..4ac439e 100644
--- a/core/java/android/hardware/input/VirtualTouchscreen.java
+++ b/core/java/android/hardware/input/VirtualTouchscreen.java
@@ -34,8 +34,9 @@
@SystemApi
public class VirtualTouchscreen extends VirtualInputDevice {
/** @hide */
- public VirtualTouchscreen(IVirtualDevice virtualDevice, IBinder token) {
- super(virtualDevice, token);
+ public VirtualTouchscreen(VirtualTouchscreenConfig config,
+ IVirtualDevice virtualDevice, IBinder token) {
+ super(config, virtualDevice, token);
}
/**
diff --git a/core/java/android/hardware/input/VirtualTouchscreenConfig.java b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
index aac341cc..6308459 100644
--- a/core/java/android/hardware/input/VirtualTouchscreenConfig.java
+++ b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
@@ -69,6 +69,12 @@
dest.writeInt(mHeight);
}
+ @Override
+ @NonNull
+ String additionalFieldsToString() {
+ return " width=" + mWidth + " height=" + mHeight;
+ }
+
@NonNull
public static final Creator<VirtualTouchscreenConfig> CREATOR =
new Creator<VirtualTouchscreenConfig>() {
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index d9c6bee..2419a4c 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -204,6 +204,8 @@
* the user in locked state so that a direct boot aware DPC could reset the password.
* Should not be used together with
* {@link #QUIET_MODE_DISABLE_ONLY_IF_CREDENTIAL_NOT_REQUIRED} or an exception will be thrown.
+ * This flag is currently only allowed for {@link #isManagedProfile() managed profiles};
+ * usage on other profiles may result in an Exception.
* @hide
*/
public static final int QUIET_MODE_DISABLE_DONT_ASK_CREDENTIAL = 0x2;
diff --git a/packages/SystemUI/compose/scene/Android.bp b/packages/SystemUI/compose/scene/Android.bp
index 050d1d5..3424085 100644
--- a/packages/SystemUI/compose/scene/Android.bp
+++ b/packages/SystemUI/compose/scene/Android.bp
@@ -21,12 +21,19 @@
default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
}
+filegroup {
+ name: "PlatformComposeSceneTransitionLayout-srcs",
+ srcs: [
+ "src/**/*.kt",
+ ],
+}
+
android_library {
name: "PlatformComposeSceneTransitionLayout",
manifest: "AndroidManifest.xml",
srcs: [
- "src/**/*.kt",
+ ":PlatformComposeSceneTransitionLayout-srcs",
],
static_libs: [
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 6153e19..3b999e30 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -17,18 +17,14 @@
package com.android.compose.animation.scene
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isSpecified
@@ -39,6 +35,7 @@
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
@@ -46,6 +43,7 @@
import com.android.compose.animation.scene.transformation.PropertyTransformation
import com.android.compose.animation.scene.transformation.SharedElementTransformation
import com.android.compose.ui.util.lerp
+import kotlinx.coroutines.launch
/** An element on screen, that can be composed in one or more scenes. */
internal class Element(val key: ElementKey) {
@@ -92,13 +90,20 @@
}
/** The target values of this element in a given scene. */
- class TargetValues {
+ class TargetValues(val scene: SceneKey) {
val lastValues = Values()
var targetSize by mutableStateOf(SizeUnspecified)
var targetOffset by mutableStateOf(Offset.Unspecified)
val sharedValues = SnapshotStateMap<ValueKey, SharedValue<*>>()
+
+ /**
+ * The attached [ElementNode] a Modifier.element() for a given element and scene. During
+ * composition, this set could have 0 to 2 elements. After composition and after all
+ * modifier nodes have been attached/detached, this set should contain exactly 1 element.
+ */
+ val nodes = mutableSetOf<ElementNode>()
}
/** A shared value of this element. */
@@ -125,50 +130,31 @@
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
key: ElementKey,
-): Modifier = composed {
- val sceneValues = remember(scene, key) { Element.TargetValues() }
- val element =
- // Get the element associated to [key] if it was already composed in another scene,
- // otherwise create it and add it to our Map<ElementKey, Element>. This is done inside a
- // withoutReadObservation() because there is no need to recompose when that map is mutated.
- Snapshot.withoutReadObservation {
- val element =
- layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
- val previousValues = element.sceneValues[scene.key]
- if (previousValues == null) {
- element.sceneValues[scene.key] = sceneValues
- } else if (previousValues != sceneValues) {
- error("$key was composed multiple times in $scene")
- }
+): Modifier {
+ val element: Element
+ val sceneValues: Element.TargetValues
- element
- }
-
- DisposableEffect(scene, sceneValues, element) {
- onDispose {
- element.sceneValues.remove(scene.key)
-
- // This was the last scene this element was in, so remove it from the map.
- if (element.sceneValues.isEmpty()) {
- layoutImpl.elements.remove(element.key)
- }
- }
+ // Get the element associated to [key] if it was already composed in another scene,
+ // otherwise create it and add it to our Map<ElementKey, Element>. This is done inside a
+ // withoutReadObservation() because there is no need to recompose when that map is mutated.
+ Snapshot.withoutReadObservation {
+ element = layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
+ sceneValues =
+ element.sceneValues[scene.key]
+ ?: Element.TargetValues(scene.key).also { element.sceneValues[scene.key] = it }
}
- val drawScale by
- remember(layoutImpl, element, scene, sceneValues) {
- derivedStateOf { getDrawScale(layoutImpl, element, scene, sceneValues) }
- }
-
- drawWithContent {
+ return this.then(ElementModifier(layoutImpl, element, sceneValues))
+ .drawWithContent {
if (shouldDrawElement(layoutImpl, scene, element)) {
+ val drawScale = getDrawScale(layoutImpl, element, scene, sceneValues)
if (drawScale == Scale.Default) {
- this@drawWithContent.drawContent()
+ drawContent()
} else {
scale(
drawScale.scaleX,
drawScale.scaleY,
- if (drawScale.pivot.isUnspecified) center else drawScale.pivot
+ if (drawScale.pivot.isUnspecified) center else drawScale.pivot,
) {
this@drawWithContent.drawContent()
}
@@ -186,6 +172,84 @@
.testTag(key.testTag)
}
+/**
+ * An element associated to [ElementNode]. Note that this element does not support updates as its
+ * arguments should always be the same.
+ */
+private data class ElementModifier(
+ private val layoutImpl: SceneTransitionLayoutImpl,
+ private val element: Element,
+ private val sceneValues: Element.TargetValues,
+) : ModifierNodeElement<ElementNode>() {
+ override fun create(): ElementNode = ElementNode(layoutImpl, element, sceneValues)
+
+ override fun update(node: ElementNode) {
+ node.update(layoutImpl, element, sceneValues)
+ }
+}
+
+internal class ElementNode(
+ layoutImpl: SceneTransitionLayoutImpl,
+ element: Element,
+ sceneValues: Element.TargetValues,
+) : Modifier.Node() {
+ private var layoutImpl: SceneTransitionLayoutImpl = layoutImpl
+ private var element: Element = element
+ private var sceneValues: Element.TargetValues = sceneValues
+
+ override fun onAttach() {
+ super.onAttach()
+ addNodeToSceneValues()
+ }
+
+ private fun addNodeToSceneValues() {
+ sceneValues.nodes.add(this)
+
+ coroutineScope.launch {
+ // At this point all [CodeLocationNode] have been attached or detached, which means that
+ // [sceneValues.codeLocations] should have exactly 1 element, otherwise this means that
+ // this element was composed multiple times in the same scene.
+ val nCodeLocations = sceneValues.nodes.size
+ if (nCodeLocations != 1 || !sceneValues.nodes.contains(this@ElementNode)) {
+ error("${element.key} was composed $nCodeLocations times in ${sceneValues.scene}")
+ }
+ }
+ }
+
+ override fun onDetach() {
+ super.onDetach()
+ removeNodeFromSceneValues()
+ }
+
+ private fun removeNodeFromSceneValues() {
+ sceneValues.nodes.remove(this)
+
+ // If element is not composed from this scene anymore, remove the scene values. This works
+ // because [onAttach] is called before [onDetach], so if an element is moved from the UI
+ // tree we will first add the new code location then remove the old one.
+ if (sceneValues.nodes.isEmpty()) {
+ element.sceneValues.remove(sceneValues.scene)
+ }
+
+ // If the element is not composed in any scene, remove it from the elements map.
+ if (element.sceneValues.isEmpty()) {
+ layoutImpl.elements.remove(element.key)
+ }
+ }
+
+ fun update(
+ layoutImpl: SceneTransitionLayoutImpl,
+ element: Element,
+ sceneValues: Element.TargetValues,
+ ) {
+ removeNodeFromSceneValues()
+ this.layoutImpl = layoutImpl
+ this.element = element
+ this.sceneValues = sceneValues
+ addNodeToSceneValues()
+ }
+}
+
private fun shouldDrawElement(
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
index bc015ee..5b752eb 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
@@ -43,7 +43,10 @@
name: String,
identity: Any = Object(),
) : Key(name, identity) {
- @VisibleForTesting val testTag: String = "scene:$name"
+ @VisibleForTesting
+ // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
+ // access internal members.
+ val testTag: String = "scene:$name"
/** The unique [ElementKey] identifying this scene's root element. */
val rootElementKey = ElementKey(name, identity)
@@ -64,7 +67,10 @@
*/
val isBackground: Boolean = false,
) : Key(name, identity), ElementMatcher {
- @VisibleForTesting val testTag: String = "element:$name"
+ @VisibleForTesting
+ // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
+ // access internal members.
+ val testTag: String = "element:$name"
override fun matches(key: ElementKey, scene: SceneKey): Boolean {
return key == this
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 1a79522..857a596 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -76,6 +76,8 @@
private val layoutImpl: SceneTransitionLayoutImpl,
private val scene: Scene,
) : SceneScope {
+ override val layoutState: SceneTransitionLayoutState = layoutImpl.state
+
override fun Modifier.element(key: ElementKey): Modifier {
return element(layoutImpl, scene, key)
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 838cb3b..c51287a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -17,7 +17,6 @@
package com.android.compose.animation.scene
import android.util.Log
-import androidx.annotation.VisibleForTesting
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
@@ -37,8 +36,7 @@
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
-@VisibleForTesting
-class SceneGestureHandler(
+internal class SceneGestureHandler(
internal val layoutImpl: SceneTransitionLayoutImpl,
internal val orientation: Orientation,
private val coroutineScope: CoroutineScope,
@@ -63,12 +61,10 @@
internal val currentScene: Scene
get() = layoutImpl.scene(transitionState.currentScene)
- @VisibleForTesting
- val isDrivingTransition
+ internal val isDrivingTransition
get() = transitionState == swipeTransition
- @VisibleForTesting
- var isAnimatingOffset
+ internal var isAnimatingOffset
get() = swipeTransition.isAnimatingOffset
private set(value) {
swipeTransition.isAnimatingOffset = value
@@ -81,7 +77,7 @@
* The velocity threshold at which the intent of the user is to swipe up or down. It is the same
* as SwipeableV2Defaults.VelocityThreshold.
*/
- @VisibleForTesting val velocityThreshold = with(layoutImpl.density) { 125.dp.toPx() }
+ internal val velocityThreshold = with(layoutImpl.density) { 125.dp.toPx() }
/**
* The positional threshold at which the intent of the user is to swipe to the next scene. It is
@@ -533,8 +529,7 @@
}
}
-@VisibleForTesting
-class SceneNestedScrollHandler(
+internal class SceneNestedScrollHandler(
private val gestureHandler: SceneGestureHandler,
private val startBehavior: NestedScrollBehavior,
private val endBehavior: NestedScrollBehavior,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 9c31445..30d13df 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -57,30 +57,17 @@
@FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f,
scenes: SceneTransitionLayoutScope.() -> Unit,
) {
- val density = LocalDensity.current
- val coroutineScope = rememberCoroutineScope()
- val layoutImpl = remember {
- SceneTransitionLayoutImpl(
- onChangeScene = onChangeScene,
- builder = scenes,
- transitions = transitions,
- state = state,
- density = density,
- edgeDetector = edgeDetector,
- transitionInterceptionThreshold = transitionInterceptionThreshold,
- coroutineScope = coroutineScope,
- )
- }
-
- layoutImpl.onChangeScene = onChangeScene
- layoutImpl.transitions = transitions
- layoutImpl.density = density
- layoutImpl.edgeDetector = edgeDetector
- layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold
-
- layoutImpl.setScenes(scenes)
- layoutImpl.setCurrentScene(currentScene)
- layoutImpl.Content(modifier)
+ SceneTransitionLayoutForTesting(
+ currentScene,
+ onChangeScene,
+ transitions,
+ state,
+ edgeDetector,
+ transitionInterceptionThreshold,
+ modifier,
+ onLayoutImpl = null,
+ scenes,
+ )
}
interface SceneTransitionLayoutScope {
@@ -108,6 +95,9 @@
@ElementDsl
interface SceneScope {
+ /** The state of the [SceneTransitionLayout] in which this scene is contained. */
+ val layoutState: SceneTransitionLayoutState
+
/**
* Tag an element identified by [key].
*
@@ -228,3 +218,47 @@
Left(Orientation.Horizontal),
Right(Orientation.Horizontal),
}
+
+/**
+ * An internal version of [SceneTransitionLayout] to be used for tests.
+ *
+ * Important: You should use this only in tests and if you need to access the underlying
+ * [SceneTransitionLayoutImpl]. In other cases, you should use [SceneTransitionLayout].
+ */
+@Composable
+internal fun SceneTransitionLayoutForTesting(
+ currentScene: SceneKey,
+ onChangeScene: (SceneKey) -> Unit,
+ transitions: SceneTransitions,
+ state: SceneTransitionLayoutState,
+ edgeDetector: EdgeDetector,
+ transitionInterceptionThreshold: Float,
+ modifier: Modifier,
+ onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)?,
+ scenes: SceneTransitionLayoutScope.() -> Unit,
+) {
+ val density = LocalDensity.current
+ val coroutineScope = rememberCoroutineScope()
+ val layoutImpl = remember {
+ SceneTransitionLayoutImpl(
+ onChangeScene = onChangeScene,
+ builder = scenes,
+ transitions = transitions,
+ state = state,
+ density = density,
+ edgeDetector = edgeDetector,
+ transitionInterceptionThreshold = transitionInterceptionThreshold,
+ coroutineScope = coroutineScope,
+ )
+ .also { onLayoutImpl?.invoke(it) }
+ }
+
+ layoutImpl.onChangeScene = onChangeScene
+ layoutImpl.transitions = transitions
+ layoutImpl.density = density
+ layoutImpl.edgeDetector = edgeDetector
+
+ layoutImpl.setScenes(scenes)
+ layoutImpl.setCurrentScene(currentScene)
+ layoutImpl.Content(modifier)
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 94f2737..60f385a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -17,7 +17,6 @@
package com.android.compose.animation.scene
import androidx.activity.compose.BackHandler
-import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
@@ -42,8 +41,7 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
-@VisibleForTesting
-class SceneTransitionLayoutImpl(
+internal class SceneTransitionLayoutImpl(
onChangeScene: (SceneKey) -> Unit,
builder: SceneTransitionLayoutScope.() -> Unit,
transitions: SceneTransitions,
@@ -260,8 +258,7 @@
internal fun isSceneReady(scene: SceneKey): Boolean = readyScenes.containsKey(scene)
- @VisibleForTesting
- fun setScenesTargetSizeForTest(size: IntSize) {
+ internal fun setScenesTargetSizeForTest(size: IntSize) {
scenes.values.forEach { it.targetSize = size }
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index b9f83c5..64c9775 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -30,6 +30,22 @@
*/
var transitionState: TransitionState by mutableStateOf(TransitionState.Idle(initialScene))
internal set
+
+ /**
+ * Whether we are transitioning, optionally restricting the check to the transition between
+ * [from] and [to].
+ */
+ fun isTransitioning(from: SceneKey? = null, to: SceneKey? = null): Boolean {
+ val transition = transitionState as? TransitionState.Transition ?: return false
+
+ // TODO(b/310915136): Remove this check.
+ if (transition.fromScene == transition.toScene) {
+ return false
+ }
+
+ return (from == null || transition.fromScene == from) &&
+ (to == null || transition.toScene == to)
+ }
}
sealed interface TransitionState {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index 72a2d61..2172ed3 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -16,7 +16,6 @@
package com.android.compose.animation.scene
-import androidx.annotation.VisibleForTesting
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.snap
import androidx.compose.ui.geometry.Offset
@@ -38,12 +37,11 @@
/** The transitions configuration of a [SceneTransitionLayout]. */
class SceneTransitions(
- @get:VisibleForTesting val transitionSpecs: List<TransitionSpec>,
+ internal val transitionSpecs: List<TransitionSpec>,
) {
private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpec>>()
- @VisibleForTesting
- fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec {
+ internal fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec {
return cache.getOrPut(from) { mutableMapOf() }.getOrPut(to) { findSpec(from, to) }
}
diff --git a/packages/SystemUI/compose/scene/tests/Android.bp b/packages/SystemUI/compose/scene/tests/Android.bp
index 6de7550..13df35b 100644
--- a/packages/SystemUI/compose/scene/tests/Android.bp
+++ b/packages/SystemUI/compose/scene/tests/Android.bp
@@ -30,10 +30,13 @@
srcs: [
"src/**/*.kt",
+
+ // TODO(b/240432457): Depend on PlatformComposeSceneTransitionLayout
+ // directly once Kotlin tests can access internal declarations.
+ ":PlatformComposeSceneTransitionLayout-srcs",
],
static_libs: [
- "PlatformComposeSceneTransitionLayout",
"PlatformComposeSceneTransitionLayoutTestsUtils",
"androidx.test.runner",
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 6401bb3..cc7a0b8 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -18,9 +18,15 @@
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.intermediateLayout
@@ -29,6 +35,7 @@
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -209,4 +216,215 @@
}
}
}
+
+ @Test
+ fun elementIsReusedInSameSceneAndBetweenScenes() {
+ var currentScene by mutableStateOf(TestScenes.SceneA)
+ var sceneCState by mutableStateOf(0)
+ var sceneDState by mutableStateOf(0)
+ val key = TestElements.Foo
+ var nullableLayoutImpl: SceneTransitionLayoutImpl? = null
+
+ rule.setContent {
+ SceneTransitionLayoutForTesting(
+ currentScene = currentScene,
+ onChangeScene = { currentScene = it },
+ transitions = remember { transitions {} },
+ state = remember { SceneTransitionLayoutState(currentScene) },
+ edgeDetector = DefaultEdgeDetector,
+ modifier = Modifier,
+ transitionInterceptionThreshold = 0f,
+ onLayoutImpl = { nullableLayoutImpl = it },
+ ) {
+ scene(TestScenes.SceneA) { /* Nothing */}
+ scene(TestScenes.SceneB) { Box(Modifier.element(key)) }
+ scene(TestScenes.SceneC) {
+ when (sceneCState) {
+ 0 -> Row(Modifier.element(key)) {}
+ 1 -> Column(Modifier.element(key)) {}
+ else -> {
+ /* Nothing */
+ }
+ }
+ }
+ scene(TestScenes.SceneD) {
+ // We should be able to extract the modifier before assigning it to different
+ // nodes.
+ val childModifier = Modifier.element(key)
+ when (sceneDState) {
+ 0 -> Row(childModifier) {}
+ 1 -> Column(childModifier) {}
+ else -> {
+ /* Nothing */
+ }
+ }
+ }
+ }
+ }
+
+ assertThat(nullableLayoutImpl).isNotNull()
+ val layoutImpl = nullableLayoutImpl!!
+
+ // Scene A: no elements in the elements map.
+ rule.waitForIdle()
+ assertThat(layoutImpl.elements).isEmpty()
+
+ // Scene B: element is in the map.
+ currentScene = TestScenes.SceneB
+ rule.waitForIdle()
+
+ assertThat(layoutImpl.elements.keys).containsExactly(key)
+ val element = layoutImpl.elements.getValue(key)
+ assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneB)
+
+ // Scene C, state 0: the same element is reused.
+ currentScene = TestScenes.SceneC
+ sceneCState = 0
+ rule.waitForIdle()
+
+ assertThat(layoutImpl.elements.keys).containsExactly(key)
+ assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
+ assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneC)
+
+ // Scene C, state 1: the same element is reused.
+ sceneCState = 1
+ rule.waitForIdle()
+
+ assertThat(layoutImpl.elements.keys).containsExactly(key)
+ assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
+ assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneC)
+
+ // Scene D, state 0: the same element is reused.
+ currentScene = TestScenes.SceneD
+ sceneDState = 0
+ rule.waitForIdle()
+
+ assertThat(layoutImpl.elements.keys).containsExactly(key)
+ assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
+ assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneD)
+
+ // Scene D, state 1: the same element is reused.
+ sceneDState = 1
+ rule.waitForIdle()
+
+ assertThat(layoutImpl.elements.keys).containsExactly(key)
+ assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
+ assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneD)
+
+ // Scene D, state 2: the element is removed from the map.
+ sceneDState = 2
+ rule.waitForIdle()
+
+ assertThat(element.sceneValues).isEmpty()
+ assertThat(layoutImpl.elements).isEmpty()
+ }
+
+ @Test
+ fun throwsExceptionWhenElementIsComposedMultipleTimes() {
+ val key = TestElements.Foo
+
+ assertThrows(IllegalStateException::class.java) {
+ rule.setContent {
+ TestSceneScope {
+ Column {
+ Box(Modifier.element(key))
+ Box(Modifier.element(key))
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun throwsExceptionWhenElementIsComposedMultipleTimes_childModifier() {
+ val key = TestElements.Foo
+
+ assertThrows(IllegalStateException::class.java) {
+ rule.setContent {
+ TestSceneScope {
+ Column {
+ val childModifier = Modifier.element(key)
+ Box(childModifier)
+ Box(childModifier)
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun throwsExceptionWhenElementIsComposedMultipleTimes_childModifier_laterDuplication() {
+ val key = TestElements.Foo
+
+ assertThrows(IllegalStateException::class.java) {
+ var nElements by mutableStateOf(1)
+ rule.setContent {
+ TestSceneScope {
+ Column {
+ val childModifier = Modifier.element(key)
+ repeat(nElements) { Box(childModifier) }
+ }
+ }
+ }
+
+ nElements = 2
+ rule.waitForIdle()
+ }
+ }
+
+ @Test
+ fun throwsExceptionWhenElementIsComposedMultipleTimes_updatedNode() {
+ assertThrows(IllegalStateException::class.java) {
+ var key by mutableStateOf(TestElements.Foo)
+ rule.setContent {
+ TestSceneScope {
+ Column {
+ Box(Modifier.element(key))
+ Box(Modifier.element(TestElements.Bar))
+ }
+ }
+ }
+
+ key = TestElements.Bar
+ rule.waitForIdle()
+ }
+ }
+
+ @Test
+ fun elementModifierSupportsUpdates() {
+ var key by mutableStateOf(TestElements.Foo)
+ var nullableLayoutImpl: SceneTransitionLayoutImpl? = null
+
+ rule.setContent {
+ SceneTransitionLayoutForTesting(
+ currentScene = TestScenes.SceneA,
+ onChangeScene = {},
+ transitions = remember { transitions {} },
+ state = remember { SceneTransitionLayoutState(TestScenes.SceneA) },
+ edgeDetector = DefaultEdgeDetector,
+ modifier = Modifier,
+ transitionInterceptionThreshold = 0f,
+ onLayoutImpl = { nullableLayoutImpl = it },
+ ) {
+ scene(TestScenes.SceneA) { Box(Modifier.element(key)) }
+ }
+ }
+
+ assertThat(nullableLayoutImpl).isNotNull()
+ val layoutImpl = nullableLayoutImpl!!
+
+ // There is only Foo in the elements map.
+ assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo)
+ val fooElement = layoutImpl.elements.getValue(TestElements.Foo)
+ assertThat(fooElement.sceneValues.keys).containsExactly(TestScenes.SceneA)
+
+ key = TestElements.Bar
+
+ // There is only Bar in the elements map and foo scene values was cleaned up.
+ rule.waitForIdle()
+ assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Bar)
+ val barElement = layoutImpl.elements.getValue(TestElements.Bar)
+ assertThat(barElement.sceneValues.keys).containsExactly(TestScenes.SceneA)
+ assertThat(fooElement.sceneValues).isEmpty()
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
new file mode 100644
index 0000000..94c51ca
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 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.compose.animation.scene
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SceneTransitionLayoutStateTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun isTransitioningTo_idle() {
+ val state = SceneTransitionLayoutState(TestScenes.SceneA)
+
+ assertThat(state.isTransitioning()).isFalse()
+ assertThat(state.isTransitioning(from = TestScenes.SceneA)).isFalse()
+ assertThat(state.isTransitioning(to = TestScenes.SceneB)).isFalse()
+ assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB))
+ .isFalse()
+ }
+
+ @Test
+ fun isTransitioningTo_fromSceneEqualToToScene() {
+ val state = SceneTransitionLayoutState(TestScenes.SceneA)
+ state.transitionState = transition(from = TestScenes.SceneA, to = TestScenes.SceneA)
+
+ assertThat(state.isTransitioning()).isFalse()
+ assertThat(state.isTransitioning(from = TestScenes.SceneA)).isFalse()
+ assertThat(state.isTransitioning(to = TestScenes.SceneB)).isFalse()
+ assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB))
+ .isFalse()
+ }
+
+ @Test
+ fun isTransitioningTo_transition() {
+ val state = SceneTransitionLayoutState(TestScenes.SceneA)
+ state.transitionState = transition(from = TestScenes.SceneA, to = TestScenes.SceneB)
+
+ assertThat(state.isTransitioning()).isTrue()
+ assertThat(state.isTransitioning(from = TestScenes.SceneA)).isTrue()
+ assertThat(state.isTransitioning(from = TestScenes.SceneB)).isFalse()
+ assertThat(state.isTransitioning(to = TestScenes.SceneB)).isTrue()
+ assertThat(state.isTransitioning(to = TestScenes.SceneA)).isFalse()
+ assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
+ }
+
+ private fun transition(from: SceneKey, to: SceneKey): TransitionState.Transition {
+ return object : TransitionState.Transition {
+ override val currentScene: SceneKey = from
+ override val fromScene: SceneKey = from
+ override val toScene: SceneKey = to
+ override val progress: Float = 0f
+ override val isInitiatedByUserInput: Boolean = false
+ override val isUserInputOngoing: Boolean = false
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt
index b4c393e..b83705a 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt
@@ -26,6 +26,7 @@
val SceneA = SceneKey("SceneA")
val SceneB = SceneKey("SceneB")
val SceneC = SceneKey("SceneC")
+ val SceneD = SceneKey("SceneD")
}
/** Element keys that can be reused by tests. */
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 81a570f..4e14c90 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1388,10 +1388,32 @@
final long identity = Binder.clearCallingIdentity();
try {
+ // QUIET_MODE_DISABLE_DONT_ASK_CREDENTIAL is only allowed for managed-profiles
+ if (dontAskCredential) {
+ UserInfo userInfo;
+ synchronized (mUsersLock) {
+ userInfo = getUserInfo(userId);
+ }
+ if (!userInfo.isManagedProfile()) {
+ throw new IllegalArgumentException("Invalid flags: " + flags
+ + ". Can't skip credential check for the user");
+ }
+ }
if (enableQuietMode) {
setQuietModeEnabled(userId, true /* enableQuietMode */, target, callingPackage);
return true;
}
+ if (android.os.Flags.allowPrivateProfile()) {
+ final UserProperties userProperties = getUserPropertiesInternal(userId);
+ if (userProperties != null
+ && userProperties.isAuthAlwaysRequiredToDisableQuietMode()) {
+ if (onlyIfCredentialNotRequired) {
+ return false;
+ }
+ showConfirmCredentialToDisableQuietMode(userId, target);
+ return false;
+ }
+ }
final boolean hasUnifiedChallenge =
mLockPatternUtils.isManagedProfileWithUnifiedChallenge(userId);
if (hasUnifiedChallenge) {
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index 29e0c35..7da76c1 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -193,6 +193,7 @@
.setStartWithParent(true)
.setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE)
.setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE)
+ .setAuthAlwaysRequiredToDisableQuietMode(false)
.setCredentialShareableWithParent(true));
}
@@ -292,7 +293,8 @@
.setDefaultSecureSettings(getDefaultNonManagedProfileSecureSettings())
.setDefaultUserProperties(new UserProperties.Builder()
.setStartWithParent(true)
- .setCredentialShareableWithParent(false)
+ .setCredentialShareableWithParent(true)
+ .setAuthAlwaysRequiredToDisableQuietMode(true)
.setMediaSharedWithParent(false)
.setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE)
.setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE)
diff --git a/services/tests/servicestests/res/xml/usertypes_test_profile.xml b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
index ef19ba1..e89199d 100644
--- a/services/tests/servicestests/res/xml/usertypes_test_profile.xml
+++ b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
@@ -39,6 +39,7 @@
crossProfileIntentResolutionStrategy='0'
mediaSharedWithParent='true'
credentialShareableWithParent='false'
+ authAlwaysRequiredToDisableQuietMode='true'
showInSettings='23'
hideInSettingsInQuietMode='true'
inheritDevicePolicy='450'
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
index c684a7b..57b1225 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -67,6 +67,7 @@
.setCrossProfileIntentResolutionStrategy(0)
.setMediaSharedWithParent(false)
.setCredentialShareableWithParent(true)
+ .setAuthAlwaysRequiredToDisableQuietMode(false)
.setDeleteAppWithParent(false)
.setAlwaysVisible(false)
.build();
@@ -80,6 +81,7 @@
actualProps.setCrossProfileIntentResolutionStrategy(1);
actualProps.setMediaSharedWithParent(true);
actualProps.setCredentialShareableWithParent(false);
+ actualProps.setAuthAlwaysRequiredToDisableQuietMode(true);
actualProps.setDeleteAppWithParent(true);
actualProps.setAlwaysVisible(true);
@@ -123,6 +125,7 @@
.setInheritDevicePolicy(1732)
.setMediaSharedWithParent(true)
.setDeleteAppWithParent(true)
+ .setAuthAlwaysRequiredToDisableQuietMode(false)
.setAlwaysVisible(true)
.build();
final UserProperties orig = new UserProperties(defaultProps);
@@ -131,6 +134,7 @@
orig.setShowInSettings(1437);
orig.setInheritDevicePolicy(9456);
orig.setDeleteAppWithParent(false);
+ orig.setAuthAlwaysRequiredToDisableQuietMode(true);
orig.setAlwaysVisible(false);
// Test every permission level. (Currently, it's linear so it's easy.)
@@ -182,6 +186,8 @@
hasManagePermission);
assertEqualGetterOrThrows(orig::getUseParentsContacts,
copy::getUseParentsContacts, hasManagePermission);
+ assertEqualGetterOrThrows(orig::isAuthAlwaysRequiredToDisableQuietMode,
+ copy::isAuthAlwaysRequiredToDisableQuietMode, hasManagePermission);
// Items requiring hasQueryPermission - put them here using hasQueryPermission.
@@ -242,6 +248,8 @@
.isEqualTo(actual.isMediaSharedWithParent());
assertThat(expected.isCredentialShareableWithParent())
.isEqualTo(actual.isCredentialShareableWithParent());
+ assertThat(expected.isAuthAlwaysRequiredToDisableQuietMode())
+ .isEqualTo(actual.isAuthAlwaysRequiredToDisableQuietMode());
assertThat(expected.getDeleteAppWithParent()).isEqualTo(actual.getDeleteAppWithParent());
assertThat(expected.getAlwaysVisible()).isEqualTo(actual.getAlwaysVisible());
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
index 20270a8..48eb5c6 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
@@ -89,6 +89,7 @@
.setCrossProfileIntentResolutionStrategy(1)
.setMediaSharedWithParent(true)
.setCredentialShareableWithParent(false)
+ .setAuthAlwaysRequiredToDisableQuietMode(true)
.setShowInSettings(900)
.setHideInSettingsInQuietMode(true)
.setInheritDevicePolicy(340)
@@ -160,6 +161,8 @@
.getCrossProfileIntentResolutionStrategy());
assertTrue(type.getDefaultUserPropertiesReference().isMediaSharedWithParent());
assertFalse(type.getDefaultUserPropertiesReference().isCredentialShareableWithParent());
+ assertTrue(type.getDefaultUserPropertiesReference()
+ .isAuthAlwaysRequiredToDisableQuietMode());
assertEquals(900, type.getDefaultUserPropertiesReference().getShowInSettings());
assertTrue(type.getDefaultUserPropertiesReference().getHideInSettingsInQuietMode());
assertEquals(340, type.getDefaultUserPropertiesReference()
@@ -306,6 +309,7 @@
.setCrossProfileIntentResolutionStrategy(1)
.setMediaSharedWithParent(false)
.setCredentialShareableWithParent(true)
+ .setAuthAlwaysRequiredToDisableQuietMode(false)
.setShowInSettings(20)
.setHideInSettingsInQuietMode(false)
.setInheritDevicePolicy(21)
@@ -347,6 +351,8 @@
assertFalse(aospType.getDefaultUserPropertiesReference().isMediaSharedWithParent());
assertTrue(aospType.getDefaultUserPropertiesReference()
.isCredentialShareableWithParent());
+ assertFalse(aospType.getDefaultUserPropertiesReference()
+ .isAuthAlwaysRequiredToDisableQuietMode());
assertEquals(20, aospType.getDefaultUserPropertiesReference().getShowInSettings());
assertFalse(aospType.getDefaultUserPropertiesReference().getHideInSettingsInQuietMode());
assertEquals(21, aospType.getDefaultUserPropertiesReference()
@@ -394,6 +400,8 @@
assertTrue(aospType.getDefaultUserPropertiesReference().isMediaSharedWithParent());
assertFalse(aospType.getDefaultUserPropertiesReference()
.isCredentialShareableWithParent());
+ assertTrue(aospType.getDefaultUserPropertiesReference()
+ .isAuthAlwaysRequiredToDisableQuietMode());
assertEquals(23, aospType.getDefaultUserPropertiesReference().getShowInSettings());
assertTrue(aospType.getDefaultUserPropertiesReference().getHideInSettingsInQuietMode());
assertEquals(450, aospType.getDefaultUserPropertiesReference()
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index 775d42a..2b6d8ed 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -331,6 +331,9 @@
.isEqualTo(privateProfileUserProperties.isMediaSharedWithParent());
assertThat(typeProps.isCredentialShareableWithParent())
.isEqualTo(privateProfileUserProperties.isCredentialShareableWithParent());
+ assertThat(typeProps.isAuthAlwaysRequiredToDisableQuietMode())
+ .isEqualTo(privateProfileUserProperties
+ .isAuthAlwaysRequiredToDisableQuietMode());
assertThrows(SecurityException.class, privateProfileUserProperties::getDeleteAppWithParent);
// Verify private profile parent