Merge "Fix global ref table overflow issue" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 8b88689a..cbb6566 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -226,6 +226,7 @@
name: "android.security.flags-aconfig-java-host",
aconfig_declarations: "android.security.flags-aconfig",
host_supported: true,
+ mode: "test",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
@@ -255,6 +256,13 @@
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+java_aconfig_library {
+ name: "android.os.flags-aconfig-java-host",
+ aconfig_declarations: "android.os.flags-aconfig",
+ host_supported: true,
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
// VirtualDeviceManager
java_aconfig_library {
name: "android.companion.virtual.flags-aconfig-java",
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/api/current.txt b/core/api/current.txt
index 91e3a3a..4256d51 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -44755,10 +44755,16 @@
method public int getLastCauseCode();
method @Nullable public android.net.LinkProperties getLinkProperties();
method public int getNetworkType();
+ method @FlaggedApi("com.android.internal.telephony.flags.network_validation") public int getNetworkValidationStatus();
method public int getState();
method public int getTransportType();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.telephony.PreciseDataConnectionState> CREATOR;
+ field @FlaggedApi("com.android.internal.telephony.flags.network_validation") public static final int NETWORK_VALIDATION_FAILURE = 4; // 0x4
+ field @FlaggedApi("com.android.internal.telephony.flags.network_validation") public static final int NETWORK_VALIDATION_IN_PROGRESS = 2; // 0x2
+ field @FlaggedApi("com.android.internal.telephony.flags.network_validation") public static final int NETWORK_VALIDATION_NOT_REQUESTED = 1; // 0x1
+ field @FlaggedApi("com.android.internal.telephony.flags.network_validation") public static final int NETWORK_VALIDATION_SUCCESS = 3; // 0x3
+ field @FlaggedApi("com.android.internal.telephony.flags.network_validation") public static final int NETWORK_VALIDATION_UNSUPPORTED = 0; // 0x0
}
public final class RadioAccessSpecifier implements android.os.Parcelable {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index fbd2142..a7ea753 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -14837,6 +14837,7 @@
method @Deprecated public int getMtu();
method public int getMtuV4();
method public int getMtuV6();
+ method @FlaggedApi("com.android.internal.telephony.flags.network_validation") public int getNetworkValidationStatus();
method @NonNull public java.util.List<java.net.InetAddress> getPcscfAddresses();
method public int getPduSessionId();
method public int getProtocolType();
@@ -14873,6 +14874,7 @@
method @Deprecated @NonNull public android.telephony.data.DataCallResponse.Builder setMtu(int);
method @NonNull public android.telephony.data.DataCallResponse.Builder setMtuV4(int);
method @NonNull public android.telephony.data.DataCallResponse.Builder setMtuV6(int);
+ method @FlaggedApi("com.android.internal.telephony.flags.network_validation") @NonNull public android.telephony.data.DataCallResponse.Builder setNetworkValidationStatus(int);
method @NonNull public android.telephony.data.DataCallResponse.Builder setPcscfAddresses(@NonNull java.util.List<java.net.InetAddress>);
method @NonNull public android.telephony.data.DataCallResponse.Builder setPduSessionId(@IntRange(from=android.telephony.data.DataCallResponse.PDU_SESSION_ID_NOT_SET, to=15) int);
method @NonNull public android.telephony.data.DataCallResponse.Builder setProtocolType(int);
@@ -14952,6 +14954,7 @@
method public final void notifyDataCallListChanged(java.util.List<android.telephony.data.DataCallResponse>);
method public final void notifyDataProfileUnthrottled(@NonNull android.telephony.data.DataProfile);
method public void requestDataCallList(@NonNull android.telephony.data.DataServiceCallback);
+ method @FlaggedApi("com.android.internal.telephony.flags.network_validation") public void requestValidation(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method public void setDataProfile(@NonNull java.util.List<android.telephony.data.DataProfile>, boolean, @NonNull android.telephony.data.DataServiceCallback);
method public void setInitialAttachApn(@NonNull android.telephony.data.DataProfile, boolean, @NonNull android.telephony.data.DataServiceCallback);
method public void setupDataCall(int, @NonNull android.telephony.data.DataProfile, boolean, boolean, int, @Nullable android.net.LinkProperties, @NonNull android.telephony.data.DataServiceCallback);
@@ -15013,6 +15016,7 @@
method public final int getSlotIndex();
method public void reportEmergencyDataNetworkPreferredTransportChanged(int);
method public void reportThrottleStatusChanged(@NonNull java.util.List<android.telephony.data.ThrottleStatus>);
+ method @FlaggedApi("com.android.internal.telephony.flags.network_validation") public void requestNetworkValidation(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method public final void updateQualifiedNetworkTypes(int, @NonNull java.util.List<java.lang.Integer>);
}
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/Intent.java b/core/java/android/content/Intent.java
index ea54c91..6b39f26 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2800,6 +2800,12 @@
/**
* Broadcast Action: An application package that was previously in the stopped state has been
* started and is no longer considered stopped.
+ * <p>When a package is force-stopped, the {@link #ACTION_PACKAGE_RESTARTED} broadcast is sent
+ * and the package in the stopped state cannot self-start for any reason unless there's an
+ * explicit request to start a component in the package. The {@link #ACTION_PACKAGE_UNSTOPPED}
+ * broadcast is sent when such an explicit process start occurs and the package is taken
+ * out of the stopped state.
+ * </p>
* <ul>
* <li> {@link #EXTRA_UID} containing the integer uid assigned to the package.
* <li> {@link #EXTRA_TIME} containing the {@link SystemClock#elapsedRealtime()
@@ -2807,6 +2813,9 @@
* </ul>
*
* <p class="note">This is a protected intent that can only be sent by the system.
+ *
+ * @see ApplicationInfo#FLAG_STOPPED
+ * @see #ACTION_PACKAGE_RESTARTED
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
@FlaggedApi(android.content.pm.Flags.FLAG_STAY_STOPPED)
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/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java
index acdc0fa..d33eadc 100644
--- a/core/java/android/database/sqlite/SQLiteStatement.java
+++ b/core/java/android/database/sqlite/SQLiteStatement.java
@@ -27,7 +27,6 @@
* <p>
* This class is not thread-safe.
* </p>
- * Note that this class is unrelated to {@link SQLiteRawStatement}.
*/
public final class SQLiteStatement extends SQLiteProgram {
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
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..c0102bf 100644
--- a/core/java/android/hardware/input/VirtualKeyEvent.java
+++ b/core/java/android/hardware/input/VirtualKeyEvent.java
@@ -172,6 +172,7 @@
KeyEvent.KEYCODE_BREAK,
KeyEvent.KEYCODE_BACK,
KeyEvent.KEYCODE_FORWARD,
+ KeyEvent.KEYCODE_LANGUAGE_SWITCH,
})
@Retention(RetentionPolicy.SOURCE)
public @interface SupportedKeycode {
@@ -205,6 +206,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/net/INetworkManagementEventObserver.aidl b/core/java/android/net/INetworkManagementEventObserver.aidl
index 0a6be20..eda80c8 100644
--- a/core/java/android/net/INetworkManagementEventObserver.aidl
+++ b/core/java/android/net/INetworkManagementEventObserver.aidl
@@ -85,14 +85,14 @@
/**
* Interface data activity status is changed.
*
- * @param transportType The transport type of the data activity change.
+ * @param label label of the data activity change.
* @param active True if the interface is actively transmitting data, false if it is idle.
* @param tsNanos Elapsed realtime in nanos when the state of the network interface changed.
* @param uid Uid of this event. It represents the uid that was responsible for waking the
* radio. For those events that are reported by system itself, not from specific uid,
* use -1 for the events which means no uid.
*/
- void interfaceClassDataActivityChanged(int transportType, boolean active, long tsNanos, int uid);
+ void interfaceClassDataActivityChanged(int label, boolean active, long tsNanos, int uid);
/**
* Information about available DNS servers has been received.
diff --git a/core/java/android/os/DeadObjectException.java b/core/java/android/os/DeadObjectException.java
index e06b0f9..65ed618 100644
--- a/core/java/android/os/DeadObjectException.java
+++ b/core/java/android/os/DeadObjectException.java
@@ -19,7 +19,8 @@
/**
* The object you are calling has died, because its hosting process
- * no longer exists.
+ * no longer exists. This is also thrown for low-level binder
+ * errors.
*/
public class DeadObjectException extends RemoteException {
public DeadObjectException() {
diff --git a/core/java/android/os/DeadSystemRuntimeException.java b/core/java/android/os/DeadSystemRuntimeException.java
index 1e86924..82b1ad8 100644
--- a/core/java/android/os/DeadSystemRuntimeException.java
+++ b/core/java/android/os/DeadSystemRuntimeException.java
@@ -18,9 +18,10 @@
/**
* Exception thrown when a call into system_server resulted in a
- * DeadObjectException, meaning that the system_server has died. There's
- * nothing apps can do at this point - the system will automatically restart -
- * so there's no point in catching this.
+ * DeadObjectException, meaning that the system_server has died or
+ * experienced a low-level binder error. There's * nothing apps can
+ * do at this point - the system will automatically restart - so
+ * there's no point in catching this.
*
* @hide
*/
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index daec1721..13572fb 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -832,10 +832,16 @@
/**
* Returns true if the current process is a 64-bit runtime.
*/
+ @android.ravenwood.annotation.RavenwoodReplace
public static final boolean is64Bit() {
return VMRuntime.getRuntime().is64Bit();
}
+ /** @hide */
+ public static final boolean is64Bit$ravenwood() {
+ return "amd64".equals(System.getProperty("os.arch"));
+ }
+
private static SomeArgs sIdentity$ravenwood;
/** @hide */
@@ -906,6 +912,7 @@
* {@link #myUid()} in that a particular user will have multiple
* distinct apps running under it each with their own uid.
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static UserHandle myUserHandle() {
return UserHandle.of(UserHandle.getUserId(myUid()));
}
@@ -914,6 +921,7 @@
* Returns whether the given uid belongs to a system core component or not.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isCoreUid(int uid) {
return UserHandle.isCore(uid);
}
@@ -924,6 +932,7 @@
* @return Whether the uid corresponds to an application sandbox running in
* a specific user.
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isApplicationUid(int uid) {
return UserHandle.isApp(uid);
}
@@ -931,6 +940,7 @@
/**
* Returns whether the current process is in an isolated sandbox.
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static final boolean isIsolated() {
return isIsolated(myUid());
}
@@ -942,6 +952,7 @@
@Deprecated
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
publicAlternatives = "Use {@link #isIsolatedUid(int)} instead.")
+ @android.ravenwood.annotation.RavenwoodKeep
public static final boolean isIsolated(int uid) {
return isIsolatedUid(uid);
}
@@ -949,6 +960,7 @@
/**
* Returns whether the process with the given {@code uid} is an isolated sandbox.
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static final boolean isIsolatedUid(int uid) {
uid = UserHandle.getAppId(uid);
return (uid >= FIRST_ISOLATED_UID && uid <= LAST_ISOLATED_UID)
@@ -962,6 +974,7 @@
*/
@SystemApi(client = MODULE_LIBRARIES)
@TestApi
+ @android.ravenwood.annotation.RavenwoodKeep
public static final boolean isSdkSandboxUid(int uid) {
uid = UserHandle.getAppId(uid);
return (uid >= FIRST_SDK_SANDBOX_UID && uid <= LAST_SDK_SANDBOX_UID);
@@ -975,6 +988,7 @@
*/
@SystemApi(client = MODULE_LIBRARIES)
@TestApi
+ @android.ravenwood.annotation.RavenwoodKeep
public static final int getAppUidForSdkSandboxUid(int uid) {
return uid - (FIRST_SDK_SANDBOX_UID - FIRST_APPLICATION_UID);
}
@@ -987,6 +1001,7 @@
*/
@SystemApi(client = MODULE_LIBRARIES)
@TestApi
+ @android.ravenwood.annotation.RavenwoodKeep
public static final int toSdkSandboxUid(int uid) {
return uid + (FIRST_SDK_SANDBOX_UID - FIRST_APPLICATION_UID);
}
@@ -994,6 +1009,7 @@
/**
* Returns whether the current process is a sdk sandbox process.
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static final boolean isSdkSandbox() {
return isSdkSandboxUid(myUid());
}
diff --git a/core/java/android/os/UserHandle.java b/core/java/android/os/UserHandle.java
index cac7f3b..0644ef1 100644
--- a/core/java/android/os/UserHandle.java
+++ b/core/java/android/os/UserHandle.java
@@ -36,6 +36,7 @@
/**
* Representation of a user on the device.
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public final class UserHandle implements Parcelable {
// NOTE: keep logic in sync with system/core/libcutils/multiuser.c
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/core/java/android/util/Xml.java b/core/java/android/util/Xml.java
index 33058d8..2a33caa 100644
--- a/core/java/android/util/Xml.java
+++ b/core/java/android/util/Xml.java
@@ -26,6 +26,7 @@
import com.android.internal.util.ArtBinaryXmlSerializer;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.BinaryXmlPullParser;
import com.android.modules.utils.BinaryXmlSerializer;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
@@ -38,6 +39,7 @@
import org.xml.sax.XMLReader;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
import java.io.BufferedInputStream;
@@ -115,6 +117,7 @@
/**
* Returns a new pull parser with namespace support.
*/
+ @android.ravenwood.annotation.RavenwoodReplace
public static XmlPullParser newPullParser() {
try {
XmlPullParser parser = XmlObjectFactory.newXmlPullParser();
@@ -126,6 +129,12 @@
}
}
+ /** @hide */
+ public static XmlPullParser newPullParser$ravenwood() {
+ // TODO: remove once we're linking against libcore
+ return new BinaryXmlPullParser();
+ }
+
/**
* Creates a new {@link TypedXmlPullParser} which is optimized for use
* inside the system, typically by supporting only a basic set of features.
@@ -136,10 +145,17 @@
* @hide
*/
@SuppressWarnings("AndroidFrameworkEfficientXml")
+ @android.ravenwood.annotation.RavenwoodReplace
public static @NonNull TypedXmlPullParser newFastPullParser() {
return XmlUtils.makeTyped(newPullParser());
}
+ /** @hide */
+ public static TypedXmlPullParser newFastPullParser$ravenwood() {
+ // TODO: remove once we're linking against libcore
+ return new BinaryXmlPullParser();
+ }
+
/**
* Creates a new {@link XmlPullParser} that reads XML documents using a
* custom binary wire protocol which benchmarking has shown to be 8.5x
@@ -148,10 +164,17 @@
*
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodReplace
public static @NonNull TypedXmlPullParser newBinaryPullParser() {
return new ArtBinaryXmlPullParser();
}
+ /** @hide */
+ public static TypedXmlPullParser newBinaryPullParser$ravenwood() {
+ // TODO: remove once we're linking against libcore
+ return new BinaryXmlPullParser();
+ }
+
/**
* Creates a new {@link XmlPullParser} which is optimized for use inside the
* system, typically by supporting only a basic set of features.
@@ -166,6 +189,7 @@
*
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodReplace
public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in)
throws IOException {
final byte[] magic = new byte[4];
@@ -198,13 +222,33 @@
return xml;
}
+ /** @hide */
+ public static @NonNull TypedXmlPullParser resolvePullParser$ravenwood(@NonNull InputStream in)
+ throws IOException {
+ // TODO: remove once we're linking against libcore
+ final TypedXmlPullParser xml = new BinaryXmlPullParser();
+ try {
+ xml.setInput(in, StandardCharsets.UTF_8.name());
+ } catch (XmlPullParserException e) {
+ throw new IOException(e);
+ }
+ return xml;
+ }
+
/**
* Creates a new xml serializer.
*/
+ @android.ravenwood.annotation.RavenwoodReplace
public static XmlSerializer newSerializer() {
return XmlObjectFactory.newXmlSerializer();
}
+ /** @hide */
+ public static XmlSerializer newSerializer$ravenwood() {
+ // TODO: remove once we're linking against libcore
+ return new BinaryXmlSerializer();
+ }
+
/**
* Creates a new {@link XmlSerializer} which is optimized for use inside the
* system, typically by supporting only a basic set of features.
@@ -215,10 +259,17 @@
* @hide
*/
@SuppressWarnings("AndroidFrameworkEfficientXml")
+ @android.ravenwood.annotation.RavenwoodReplace
public static @NonNull TypedXmlSerializer newFastSerializer() {
return XmlUtils.makeTyped(new FastXmlSerializer());
}
+ /** @hide */
+ public static @NonNull TypedXmlSerializer newFastSerializer$ravenwood() {
+ // TODO: remove once we're linking against libcore
+ return new BinaryXmlSerializer();
+ }
+
/**
* Creates a new {@link XmlSerializer} that writes XML documents using a
* custom binary wire protocol which benchmarking has shown to be 4.4x
@@ -227,10 +278,17 @@
*
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodReplace
public static @NonNull TypedXmlSerializer newBinarySerializer() {
return new ArtBinaryXmlSerializer();
}
+ /** @hide */
+ public static @NonNull TypedXmlSerializer newBinarySerializer$ravenwood() {
+ // TODO: remove once we're linking against libcore
+ return new BinaryXmlSerializer();
+ }
+
/**
* Creates a new {@link XmlSerializer} which is optimized for use inside the
* system, typically by supporting only a basic set of features.
@@ -245,6 +303,7 @@
*
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodReplace
public static @NonNull TypedXmlSerializer resolveSerializer(@NonNull OutputStream out)
throws IOException {
final TypedXmlSerializer xml;
@@ -257,6 +316,15 @@
return xml;
}
+ /** @hide */
+ public static @NonNull TypedXmlSerializer resolveSerializer$ravenwood(@NonNull OutputStream out)
+ throws IOException {
+ // TODO: remove once we're linking against libcore
+ final TypedXmlSerializer xml = new BinaryXmlSerializer();
+ xml.setOutput(out, StandardCharsets.UTF_8.name());
+ return xml;
+ }
+
/**
* Copy the first XML document into the second document.
* <p>
diff --git a/core/java/com/android/internal/util/ArtFastDataInput.java b/core/java/com/android/internal/util/ArtFastDataInput.java
index 3e8916c..768ea82 100644
--- a/core/java/com/android/internal/util/ArtFastDataInput.java
+++ b/core/java/com/android/internal/util/ArtFastDataInput.java
@@ -21,6 +21,8 @@
import com.android.modules.utils.FastDataInput;
+import dalvik.system.VMRuntime;
+
import java.io.DataInput;
import java.io.IOException;
import java.io.InputStream;
@@ -35,13 +37,14 @@
*/
public class ArtFastDataInput extends FastDataInput {
private static AtomicReference<ArtFastDataInput> sInCache = new AtomicReference<>();
+ private static VMRuntime sRuntime = VMRuntime.getRuntime();
private final long mBufferPtr;
public ArtFastDataInput(@NonNull InputStream in, int bufferSize) {
super(in, bufferSize);
- mBufferPtr = mRuntime.addressOf(mBuffer);
+ mBufferPtr = sRuntime.addressOf(mBuffer);
}
/**
@@ -66,6 +69,7 @@
* Release a {@link ArtFastDataInput} to potentially be recycled. You must not
* interact with the object after releasing it.
*/
+ @Override
public void release() {
super.release();
@@ -76,6 +80,11 @@
}
@Override
+ public byte[] newByteArray(int bufferSize) {
+ return (byte[]) sRuntime.newNonMovableArray(byte.class, bufferSize);
+ }
+
+ @Override
public String readUTF() throws IOException {
// Attempt to read directly from buffer space if there's enough room,
// otherwise fall back to chunking into place
@@ -86,9 +95,9 @@
mBufferPos += len;
return res;
} else {
- final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
+ final byte[] tmp = (byte[]) sRuntime.newNonMovableArray(byte.class, len + 1);
readFully(tmp, 0, len);
- return CharsetUtils.fromModifiedUtf8Bytes(mRuntime.addressOf(tmp), 0, len);
+ return CharsetUtils.fromModifiedUtf8Bytes(sRuntime.addressOf(tmp), 0, len);
}
}
}
diff --git a/core/java/com/android/internal/util/ArtFastDataOutput.java b/core/java/com/android/internal/util/ArtFastDataOutput.java
index ac595b6..360ddb8 100644
--- a/core/java/com/android/internal/util/ArtFastDataOutput.java
+++ b/core/java/com/android/internal/util/ArtFastDataOutput.java
@@ -21,6 +21,8 @@
import com.android.modules.utils.FastDataOutput;
+import dalvik.system.VMRuntime;
+
import java.io.DataOutput;
import java.io.IOException;
import java.io.OutputStream;
@@ -35,13 +37,14 @@
*/
public class ArtFastDataOutput extends FastDataOutput {
private static AtomicReference<ArtFastDataOutput> sOutCache = new AtomicReference<>();
+ private static VMRuntime sRuntime = VMRuntime.getRuntime();
private final long mBufferPtr;
public ArtFastDataOutput(@NonNull OutputStream out, int bufferSize) {
super(out, bufferSize);
- mBufferPtr = mRuntime.addressOf(mBuffer);
+ mBufferPtr = sRuntime.addressOf(mBuffer);
}
/**
@@ -73,6 +76,11 @@
}
@Override
+ public byte[] newByteArray(int bufferSize) {
+ return (byte[]) sRuntime.newNonMovableArray(byte.class, bufferSize);
+ }
+
+ @Override
public void writeUTF(String s) throws IOException {
// Attempt to write directly to buffer space if there's enough room,
// otherwise fall back to chunking into place
@@ -94,8 +102,8 @@
// Negative value indicates buffer was too small and we need to
// allocate a temporary buffer for encoding
len = -len;
- final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
- CharsetUtils.toModifiedUtf8Bytes(s, mRuntime.addressOf(tmp), 0, tmp.length);
+ final byte[] tmp = (byte[]) sRuntime.newNonMovableArray(byte.class, len + 1);
+ CharsetUtils.toModifiedUtf8Bytes(s, sRuntime.addressOf(tmp), 0, tmp.length);
writeShort(len);
write(tmp, 0, len);
}
diff --git a/core/java/com/android/server/net/BaseNetworkObserver.java b/core/java/com/android/server/net/BaseNetworkObserver.java
index 139b88b..61e017d 100644
--- a/core/java/com/android/server/net/BaseNetworkObserver.java
+++ b/core/java/com/android/server/net/BaseNetworkObserver.java
@@ -64,7 +64,7 @@
}
@Override
- public void interfaceClassDataActivityChanged(int transportType, boolean active, long tsNanos,
+ public void interfaceClassDataActivityChanged(int label, boolean active, long tsNanos,
int uid) {
// default no-op
}
diff --git a/core/jni/android_media_AudioRecord.cpp b/core/jni/android_media_AudioRecord.cpp
index b1dab85..8fa179b 100644
--- a/core/jni/android_media_AudioRecord.cpp
+++ b/core/jni/android_media_AudioRecord.cpp
@@ -574,7 +574,7 @@
if (result != NO_ERROR) {
return -1;
}
- return frameCount * channelCount * audio_bytes_per_sample(format);
+ return frameCount * audio_bytes_per_frame(channelCount, format);
}
static jboolean android_media_AudioRecord_setInputDevice(
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index dc2b056..b065611 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -415,12 +415,6 @@
"group": "WM_DEBUG_WINDOW_TRANSITIONS",
"at": "com\/android\/server\/wm\/Transition.java"
},
- "-1717147904": {
- "message": "Current focused window is embeddedWindow. Dispatch KEYCODE_BACK.",
- "level": "DEBUG",
- "group": "WM_DEBUG_BACK_PREVIEW",
- "at": "com\/android\/server\/wm\/BackNavigationController.java"
- },
"-1710206702": {
"message": "Display id=%d is frozen while keyguard locked, return %d",
"level": "VERBOSE",
@@ -1213,12 +1207,6 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/ActivityRecord.java"
},
- "-997565097": {
- "message": "Focused window found using getFocusedWindowToken",
- "level": "DEBUG",
- "group": "WM_DEBUG_BACK_PREVIEW",
- "at": "com\/android\/server\/wm\/BackNavigationController.java"
- },
"-993378225": {
"message": "finishDrawingLocked: mDrawState=COMMIT_DRAW_PENDING %s in %s",
"level": "VERBOSE",
@@ -2233,6 +2221,12 @@
"group": "WM_DEBUG_RECENTS_ANIMATIONS",
"at": "com\/android\/server\/wm\/RecentsAnimation.java"
},
+ "-98422345": {
+ "message": "Focus window is closing.",
+ "level": "DEBUG",
+ "group": "WM_DEBUG_BACK_PREVIEW",
+ "at": "com\/android\/server\/wm\/BackNavigationController.java"
+ },
"-91393839": {
"message": "Set animatingExit: reason=remove\/applyAnimation win=%s",
"level": "VERBOSE",
@@ -2731,12 +2725,6 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/ActivityRecord.java"
},
- "309039362": {
- "message": "SURFACE MATRIX [%f,%f,%f,%f]: %s",
- "level": "INFO",
- "group": "WM_SHOW_TRANSACTIONS",
- "at": "com\/android\/server\/wm\/WindowSurfaceController.java"
- },
"312030608": {
"message": "New topFocusedDisplayId=%d",
"level": "DEBUG",
@@ -3091,12 +3079,6 @@
"group": "WM_ERROR",
"at": "com\/android\/server\/wm\/WindowManagerService.java"
},
- "633654009": {
- "message": "SURFACE POS (setPositionInTransaction) @ (%f,%f): %s",
- "level": "INFO",
- "group": "WM_SHOW_TRANSACTIONS",
- "at": "com\/android\/server\/wm\/WindowSurfaceController.java"
- },
"638429464": {
"message": "\tRemove container=%s",
"level": "DEBUG",
diff --git a/data/keyboards/Generic.kl b/data/keyboards/Generic.kl
index 51b720d..f9347ee 100644
--- a/data/keyboards/Generic.kl
+++ b/data/keyboards/Generic.kl
@@ -324,7 +324,7 @@
# key 365 "KEY_EPG"
key 366 DVR
# key 367 "KEY_MHP"
-# key 368 "KEY_LANGUAGE"
+key 368 LANGUAGE_SWITCH
# key 369 "KEY_TITLE"
key 370 CAPTIONS
# key 371 "KEY_ANGLE"
diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp
index 58224b8..38bd7d5 100644
--- a/packages/PackageInstaller/Android.bp
+++ b/packages/PackageInstaller/Android.bp
@@ -46,6 +46,9 @@
"xz-java",
"androidx.leanback_leanback",
"androidx.annotation_annotation",
+ "androidx.fragment_fragment",
+ "androidx.lifecycle_lifecycle-livedata",
+ "androidx.lifecycle_lifecycle-extensions",
],
lint: {
@@ -69,6 +72,9 @@
static_libs: [
"xz-java",
"androidx.leanback_leanback",
+ "androidx.fragment_fragment",
+ "androidx.lifecycle_lifecycle-livedata",
+ "androidx.lifecycle_lifecycle-extensions",
],
aaptflags: ["--product tablet"],
@@ -94,6 +100,9 @@
"xz-java",
"androidx.leanback_leanback",
"androidx.annotation_annotation",
+ "androidx.fragment_fragment",
+ "androidx.lifecycle_lifecycle-livedata",
+ "androidx.lifecycle_lifecycle-extensions",
],
aaptflags: ["--product tv"],
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index a16f9f5..35f5772 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -43,6 +43,11 @@
</intent-filter>
</receiver>
+ <activity android:name=".v2.ui.InstallLaunch"
+ android:configChanges="orientation|keyboardHidden|screenSize"
+ android:theme="@style/Theme.AlertDialogActivity"
+ android:exported="true"/>
+
<activity android:name=".InstallStart"
android:theme="@style/Theme.AlertDialogActivity"
android:exported="true"
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
index 736e0ef..e2107eb 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
@@ -40,7 +40,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
+import com.android.packageinstaller.v2.ui.InstallLaunch;
import java.util.Arrays;
/**
@@ -57,9 +57,23 @@
private final boolean mLocalLOGV = false;
+ // TODO (sumedhsen): Replace with an Android Feature Flag once implemented
+ private static final boolean USE_PIA_V2 = false;
+
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
+ if (USE_PIA_V2) {
+ Intent piaV2 = new Intent(getIntent());
+ piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_NAME, getCallingPackage());
+ piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_UID, getLaunchedFromUid());
+ piaV2.setClass(this, InstallLaunch.class);
+ piaV2.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+ startActivity(piaV2);
+ finish();
+ return;
+ }
mPackageManager = getPackageManager();
mUserManager = getSystemService(UserManager.class);
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java
new file mode 100644
index 0000000..03af951
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java
@@ -0,0 +1,376 @@
+/*
+ * 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.packageinstaller.v2.model;
+
+import static com.android.packageinstaller.v2.model.PackageUtil.canPackageQuery;
+import static com.android.packageinstaller.v2.model.PackageUtil.isCallerSessionOwner;
+import static com.android.packageinstaller.v2.model.PackageUtil.isInstallPermissionGrantedOrRequested;
+import static com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted;
+import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_INTERNAL_ERROR;
+import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_POLICY;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.admin.DevicePolicyManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionInfo;
+import android.content.pm.PackageManager;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.util.EventLog;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.MutableLiveData;
+import com.android.packageinstaller.v2.model.installstagedata.InstallAborted;
+import com.android.packageinstaller.v2.model.installstagedata.InstallReady;
+import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
+import com.android.packageinstaller.v2.model.installstagedata.InstallStaging;
+import java.io.IOException;
+
+public class InstallRepository {
+
+ private static final String SCHEME_PACKAGE = "package";
+ private static final String TAG = InstallRepository.class.getSimpleName();
+ private final Context mContext;
+ private final PackageManager mPackageManager;
+ private final PackageInstaller mPackageInstaller;
+ private final UserManager mUserManager;
+ private final DevicePolicyManager mDevicePolicyManager;
+ private final MutableLiveData<InstallStage> mStagingResult = new MutableLiveData<>();
+ private final boolean mLocalLOGV = false;
+ private Intent mIntent;
+ private boolean mIsSessionInstall;
+ private boolean mIsTrustedSource;
+ /**
+ * Session ID for a session created when caller uses PackageInstaller APIs
+ */
+ private int mSessionId;
+ /**
+ * Session ID for a session created by this app
+ */
+ private int mStagedSessionId = SessionInfo.INVALID_ID;
+ private int mCallingUid;
+ private String mCallingPackage;
+ private SessionStager mSessionStager;
+
+ public InstallRepository(Context context) {
+ mContext = context;
+ mPackageManager = context.getPackageManager();
+ mPackageInstaller = mPackageManager.getPackageInstaller();
+ mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
+ mUserManager = context.getSystemService(UserManager.class);
+ }
+
+ /**
+ * Extracts information from the incoming install intent, checks caller's permission to install
+ * packages, verifies that the caller is the install session owner (in case of a session based
+ * install) and checks if the current user has restrictions set that prevent app installation,
+ *
+ * @param intent the incoming {@link Intent} object for installing a package
+ * @param callerInfo {@link CallerInfo} that holds the callingUid and callingPackageName
+ * @return <p>{@link InstallAborted} if there are errors while performing the checks</p>
+ * <p>{@link InstallStaging} after successfully performing the checks</p>
+ */
+ public InstallStage performPreInstallChecks(Intent intent, CallerInfo callerInfo) {
+ mIntent = intent;
+
+ String callingAttributionTag = null;
+
+ mIsSessionInstall =
+ PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction())
+ || PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction());
+
+ mSessionId = mIsSessionInstall
+ ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, SessionInfo.INVALID_ID)
+ : SessionInfo.INVALID_ID;
+
+ mCallingPackage = callerInfo.getPackageName();
+
+ if (mCallingPackage == null && mSessionId != SessionInfo.INVALID_ID) {
+ PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(mSessionId);
+ mCallingPackage = (sessionInfo != null) ? sessionInfo.getInstallerPackageName() : null;
+ callingAttributionTag =
+ (sessionInfo != null) ? sessionInfo.getInstallerAttributionTag() : null;
+ }
+
+ // Uid of the source package, coming from ActivityManager
+ mCallingUid = callerInfo.getUid();
+ if (mCallingUid == Process.INVALID_UID) {
+ Log.e(TAG, "Could not determine the launching uid.");
+ }
+ final ApplicationInfo sourceInfo = getSourceInfo(mCallingPackage);
+ // Uid of the source package, with a preference to uid from ApplicationInfo
+ final int originatingUid = sourceInfo != null ? sourceInfo.uid : mCallingUid;
+
+ if (mCallingUid == Process.INVALID_UID && sourceInfo == null) {
+ // Caller's identity could not be determined. Abort the install
+ return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
+ }
+
+ if (!isCallerSessionOwner(mPackageInstaller, originatingUid, mSessionId)) {
+ return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
+ }
+
+ mIsTrustedSource = isInstallRequestFromTrustedSource(sourceInfo, mIntent, originatingUid);
+
+ if (!isInstallPermissionGrantedOrRequested(mContext, mCallingUid, originatingUid,
+ mIsTrustedSource)) {
+ return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
+ }
+
+ String restriction = getDevicePolicyRestrictions();
+ if (restriction != null) {
+ InstallAborted.Builder abortedBuilder =
+ new InstallAborted.Builder(ABORT_REASON_POLICY).setMessage(restriction);
+ final Intent adminSupportDetailsIntent =
+ mDevicePolicyManager.createAdminSupportIntent(restriction);
+ if (adminSupportDetailsIntent != null) {
+ abortedBuilder.setResultIntent(adminSupportDetailsIntent);
+ }
+ return abortedBuilder.build();
+ }
+
+ maybeRemoveInvalidInstallerPackageName(callerInfo);
+
+ return new InstallStaging();
+ }
+
+ /**
+ * @return the ApplicationInfo for the installation source (the calling package), if available
+ */
+ @Nullable
+ private ApplicationInfo getSourceInfo(@Nullable String callingPackage) {
+ if (callingPackage == null) {
+ return null;
+ }
+ try {
+ return mPackageManager.getApplicationInfo(callingPackage, 0);
+ } catch (PackageManager.NameNotFoundException ignored) {
+ return null;
+ }
+ }
+
+ private boolean isInstallRequestFromTrustedSource(ApplicationInfo sourceInfo, Intent intent,
+ int originatingUid) {
+ boolean isNotUnknownSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false);
+ return sourceInfo != null && sourceInfo.isPrivilegedApp()
+ && (isNotUnknownSource
+ || isPermissionGranted(mContext, Manifest.permission.INSTALL_PACKAGES, originatingUid));
+ }
+
+ private String getDevicePolicyRestrictions() {
+ final String[] restrictions = new String[]{
+ UserManager.DISALLOW_INSTALL_APPS,
+ UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
+ UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY
+ };
+
+ for (String restriction : restrictions) {
+ if (!mUserManager.hasUserRestrictionForUser(restriction, Process.myUserHandle())) {
+ continue;
+ }
+ return restriction;
+ }
+ return null;
+ }
+
+ private void maybeRemoveInvalidInstallerPackageName(CallerInfo callerInfo) {
+ final String installerPackageNameFromIntent =
+ mIntent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME);
+ if (installerPackageNameFromIntent == null) {
+ return;
+ }
+ if (!TextUtils.equals(installerPackageNameFromIntent, callerInfo.getPackageName())
+ && !isPermissionGranted(mPackageManager, Manifest.permission.INSTALL_PACKAGES,
+ callerInfo.getPackageName())) {
+ Log.e(TAG, "The given installer package name " + installerPackageNameFromIntent
+ + " is invalid. Remove it.");
+ EventLog.writeEvent(0x534e4554, "236687884", callerInfo.getUid(),
+ "Invalid EXTRA_INSTALLER_PACKAGE_NAME");
+ mIntent.removeExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME);
+ }
+ }
+
+ public void stageForInstall() {
+ Uri uri = mIntent.getData();
+ if (mIsSessionInstall || (uri != null && SCHEME_PACKAGE.equals(uri.getScheme()))) {
+ // For a session based install or installing with a package:// URI, there is no file
+ // for us to stage. Setting the mStagingResult as null will signal InstallViewModel to
+ // proceed with user confirmation stage.
+ mStagingResult.setValue(new InstallReady());
+ return;
+ }
+ if (uri != null
+ && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())
+ && canPackageQuery(mContext, mCallingUid, uri)) {
+
+ if (mStagedSessionId > 0) {
+ final PackageInstaller.SessionInfo info =
+ mPackageInstaller.getSessionInfo(mStagedSessionId);
+ if (info == null || !info.isActive() || info.getResolvedBaseApkPath() == null) {
+ Log.w(TAG, "Session " + mStagedSessionId + " in funky state; ignoring");
+ if (info != null) {
+ cleanupStagingSession();
+ }
+ mStagedSessionId = 0;
+ }
+ }
+
+ // Session does not exist, or became invalid.
+ if (mStagedSessionId <= 0) {
+ // Create session here to be able to show error.
+ try (final AssetFileDescriptor afd =
+ mContext.getContentResolver().openAssetFileDescriptor(uri, "r")) {
+ ParcelFileDescriptor pfd = afd != null ? afd.getParcelFileDescriptor() : null;
+ PackageInstaller.SessionParams params =
+ createSessionParams(mIntent, pfd, uri.toString());
+ mStagedSessionId = mPackageInstaller.createSession(params);
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to create a staging session", e);
+ mStagingResult.setValue(
+ new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
+ .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT,
+ PackageManager.INSTALL_FAILED_INVALID_APK))
+ .setActivityResultCode(Activity.RESULT_FIRST_USER)
+ .build());
+ return;
+ }
+ }
+
+ SessionStageListener listener = new SessionStageListener() {
+ @Override
+ public void onStagingSuccess(SessionInfo info) {
+ //TODO: Verify if the returned sessionInfo should be used anywhere
+ mStagingResult.setValue(new InstallReady());
+ }
+
+ @Override
+ public void onStagingFailure() {
+ cleanupStagingSession();
+ mStagingResult.setValue(
+ new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
+ .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT,
+ PackageManager.INSTALL_FAILED_INVALID_APK))
+ .setActivityResultCode(Activity.RESULT_FIRST_USER)
+ .build());
+ }
+ };
+ if (mSessionStager != null) {
+ mSessionStager.cancel(true);
+ }
+ mSessionStager = new SessionStager(mContext, uri, mStagedSessionId, listener);
+ mSessionStager.execute();
+ }
+ }
+
+ private void cleanupStagingSession() {
+ if (mStagedSessionId > 0) {
+ try {
+ mPackageInstaller.abandonSession(mStagedSessionId);
+ } catch (SecurityException ignored) {
+ }
+ mStagedSessionId = 0;
+ }
+ }
+
+ private PackageInstaller.SessionParams createSessionParams(@NonNull Intent intent,
+ @Nullable ParcelFileDescriptor pfd, @NonNull String debugPathName) {
+ PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
+ PackageInstaller.SessionParams.MODE_FULL_INSTALL);
+ final Uri referrerUri = intent.getParcelableExtra(Intent.EXTRA_REFERRER, Uri.class);
+ params.setPackageSource(
+ referrerUri != null ? PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE
+ : PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE);
+ params.setInstallAsInstantApp(false);
+ params.setReferrerUri(referrerUri);
+ params.setOriginatingUri(
+ intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI, Uri.class));
+ params.setOriginatingUid(intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
+ Process.INVALID_UID));
+ params.setInstallerPackageName(intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME));
+ params.setInstallReason(PackageManager.INSTALL_REASON_USER);
+ // Disable full screen intent usage by for sideloads.
+ params.setPermissionState(Manifest.permission.USE_FULL_SCREEN_INTENT,
+ PackageInstaller.SessionParams.PERMISSION_STATE_DENIED);
+
+ if (pfd != null) {
+ try {
+ final PackageInstaller.InstallInfo result = mPackageInstaller.readInstallInfo(pfd,
+ debugPathName, 0);
+ params.setAppPackageName(result.getPackageName());
+ params.setInstallLocation(result.getInstallLocation());
+ params.setSize(result.calculateInstalledSize(params, pfd));
+ } catch (PackageInstaller.PackageParsingException e) {
+ Log.e(TAG, "Cannot parse package " + debugPathName + ". Assuming defaults.", e);
+ params.setSize(pfd.getStatSize());
+ } catch (IOException e) {
+ Log.e(TAG,
+ "Cannot calculate installed size " + debugPathName
+ + ". Try only apk size.", e);
+ }
+ } else {
+ Log.e(TAG, "Cannot parse package " + debugPathName + ". Assuming defaults.");
+ }
+ return params;
+ }
+
+ public MutableLiveData<Integer> getStagingProgress() {
+ if (mSessionStager != null) {
+ return mSessionStager.getProgress();
+ }
+ return new MutableLiveData<>(0);
+ }
+
+ public MutableLiveData<InstallStage> getStagingResult() {
+ return mStagingResult;
+ }
+
+ public interface SessionStageListener {
+
+ void onStagingSuccess(SessionInfo info);
+
+ void onStagingFailure();
+ }
+
+ public static class CallerInfo {
+
+ private final String mPackageName;
+ private final int mUid;
+
+ public CallerInfo(String packageName, int uid) {
+ mPackageName = packageName;
+ mUid = uid;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public int getUid() {
+ return mUid;
+ }
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
new file mode 100644
index 0000000..82a8c95
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
@@ -0,0 +1,215 @@
+/*
+ * 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.packageinstaller.v2.model;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Process;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import java.util.Arrays;
+
+public class PackageUtil {
+
+ private static final String TAG = InstallRepository.class.getSimpleName();
+ private static final String DOWNLOADS_AUTHORITY = "downloads";
+
+ /**
+ * Determines if the UID belongs to the system downloads provider and returns the
+ * {@link ApplicationInfo} of the provider
+ *
+ * @param uid UID of the caller
+ * @return {@link ApplicationInfo} of the provider if a downloads provider exists, it is a
+ * system app, and its UID matches with the passed UID, null otherwise.
+ */
+ public static ApplicationInfo getSystemDownloadsProviderInfo(PackageManager pm, int uid) {
+ final ProviderInfo providerInfo = pm.resolveContentProvider(
+ DOWNLOADS_AUTHORITY, 0);
+ if (providerInfo == null) {
+ // There seems to be no currently enabled downloads provider on the system.
+ return null;
+ }
+ ApplicationInfo appInfo = providerInfo.applicationInfo;
+ if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 && uid == appInfo.uid) {
+ return appInfo;
+ }
+ return null;
+ }
+
+ /**
+ * Get the maximum target sdk for a UID.
+ *
+ * @param context The context to use
+ * @param uid The UID requesting the install/uninstall
+ * @return The maximum target SDK or -1 if the uid does not match any packages.
+ */
+ public static int getMaxTargetSdkVersionForUid(@NonNull Context context, int uid) {
+ PackageManager pm = context.getPackageManager();
+ final String[] packages = pm.getPackagesForUid(uid);
+ int targetSdkVersion = -1;
+ if (packages != null) {
+ for (String packageName : packages) {
+ try {
+ ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
+ targetSdkVersion = Math.max(targetSdkVersion, info.targetSdkVersion);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Ignore and try the next package
+ }
+ }
+ }
+ return targetSdkVersion;
+ }
+
+ public static boolean canPackageQuery(Context context, int callingUid, Uri packageUri) {
+ PackageManager pm = context.getPackageManager();
+ ProviderInfo info = pm.resolveContentProvider(packageUri.getAuthority(),
+ PackageManager.ComponentInfoFlags.of(0));
+ if (info == null) {
+ return false;
+ }
+ String targetPackage = info.packageName;
+
+ String[] callingPackages = pm.getPackagesForUid(callingUid);
+ if (callingPackages == null) {
+ return false;
+ }
+ for (String callingPackage : callingPackages) {
+ try {
+ if (pm.canPackageQuery(callingPackage, targetPackage)) {
+ return true;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // no-op
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @param context the {@link Context} object
+ * @param permission the permission name to check
+ * @param callingUid the UID of the caller who's permission is being checked
+ * @return {@code true} if the callingUid is granted the said permission
+ */
+ public static boolean isPermissionGranted(Context context, String permission, int callingUid) {
+ return context.checkPermission(permission, -1, callingUid)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * @param pm the {@link PackageManager} object
+ * @param permission the permission name to check
+ * @param packageName the name of the package who's permission is being checked
+ * @return {@code true} if the package is granted the said permission
+ */
+ public static boolean isPermissionGranted(PackageManager pm, String permission,
+ String packageName) {
+ return pm.checkPermission(permission, packageName) == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * @param context the {@link Context} object
+ * @param callingUid the UID of the caller who's permission is being checked
+ * @param originatingUid the UID from where install is being originated. This could be same as
+ * callingUid or it will be the UID of the package performing a session based install
+ * @param isTrustedSource whether install request is coming from a privileged app or an app that
+ * has {@link Manifest.permission.INSTALL_PACKAGES} permission granted
+ * @return {@code true} if the package is granted the said permission
+ */
+ public static boolean isInstallPermissionGrantedOrRequested(Context context, int callingUid,
+ int originatingUid, boolean isTrustedSource) {
+ boolean isDocumentsManager =
+ isPermissionGranted(context, Manifest.permission.MANAGE_DOCUMENTS, callingUid);
+ boolean isSystemDownloadsProvider =
+ getSystemDownloadsProviderInfo(context.getPackageManager(), callingUid) != null;
+
+ if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager) {
+
+ final int targetSdkVersion = getMaxTargetSdkVersionForUid(context, originatingUid);
+ if (targetSdkVersion < 0) {
+ // Invalid originating uid supplied. Abort install.
+ Log.w(TAG, "Cannot get target sdk version for uid " + originatingUid);
+ return false;
+ } else if (targetSdkVersion >= Build.VERSION_CODES.O
+ && !isUidRequestingPermission(context.getPackageManager(), originatingUid,
+ Manifest.permission.REQUEST_INSTALL_PACKAGES)) {
+ Log.e(TAG, "Requesting uid " + originatingUid + " needs to declare permission "
+ + Manifest.permission.REQUEST_INSTALL_PACKAGES);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @param pm the {@link PackageManager} object
+ * @param uid the UID of the caller who's permission is being checked
+ * @param permission the permission name to check
+ * @return {@code true} if the caller is requesting the said permission in its Manifest
+ */
+ public static boolean isUidRequestingPermission(PackageManager pm, int uid, String permission) {
+ final String[] packageNames = pm.getPackagesForUid(uid);
+ if (packageNames == null) {
+ return false;
+ }
+ for (final String packageName : packageNames) {
+ final PackageInfo packageInfo;
+ try {
+ packageInfo = pm.getPackageInfo(packageName,
+ PackageManager.GET_PERMISSIONS);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Ignore and try the next package
+ continue;
+ }
+ if (packageInfo.requestedPermissions != null
+ && Arrays.asList(packageInfo.requestedPermissions).contains(permission)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @param pi the {@link PackageInstaller} object to use
+ * @param originatingUid the UID of the package performing a session based install
+ * @param sessionId ID of the install session
+ * @return {@code true} if the caller is the session owner
+ */
+ public static boolean isCallerSessionOwner(PackageInstaller pi, int originatingUid,
+ int sessionId) {
+ if (sessionId == SessionInfo.INVALID_ID) {
+ return false;
+ }
+ if (originatingUid == Process.ROOT_UID) {
+ return true;
+ }
+ PackageInstaller.SessionInfo sessionInfo = pi.getSessionInfo(sessionId);
+ if (sessionInfo == null) {
+ return false;
+ }
+ int installerUid = sessionInfo.getInstallerUid();
+ return originatingUid == installerUid;
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.java
new file mode 100644
index 0000000..a2c81f1
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.java
@@ -0,0 +1,126 @@
+/*
+ * 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.packageinstaller.v2.model;
+
+import static android.content.res.AssetFileDescriptor.UNKNOWN_LENGTH;
+
+import android.content.Context;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionInfo;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.util.Log;
+import androidx.lifecycle.MutableLiveData;
+import com.android.packageinstaller.v2.model.InstallRepository.SessionStageListener;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class SessionStager extends AsyncTask<Void, Integer, SessionInfo> {
+
+ private static final String TAG = SessionStager.class.getSimpleName();
+ private final Context mContext;
+ private final Uri mUri;
+ private final int mStagedSessionId;
+ private final MutableLiveData<Integer> mProgressLiveData = new MutableLiveData<>(0);
+ private final SessionStageListener mListener;
+
+ SessionStager(Context context, Uri uri, int stagedSessionId, SessionStageListener listener) {
+ mContext = context;
+ mUri = uri;
+ mStagedSessionId = stagedSessionId;
+ mListener = listener;
+ }
+
+ @Override
+ protected PackageInstaller.SessionInfo doInBackground(Void... params) {
+ PackageInstaller pi = mContext.getPackageManager().getPackageInstaller();
+ try (PackageInstaller.Session session = pi.openSession(mStagedSessionId);
+ InputStream in = mContext.getContentResolver().openInputStream(mUri)) {
+ session.setStagingProgress(0);
+
+ if (in == null) {
+ return null;
+ }
+ final long sizeBytes = getContentSizeBytes();
+ mProgressLiveData.postValue(sizeBytes > 0 ? 0 : -1);
+
+ long totalRead = 0;
+ try (OutputStream out = session.openWrite("PackageInstaller", 0, sizeBytes)) {
+ byte[] buffer = new byte[1024 * 1024];
+ while (true) {
+ int numRead = in.read(buffer);
+
+ if (numRead == -1) {
+ session.fsync(out);
+ break;
+ }
+
+ if (isCancelled()) {
+ break;
+ }
+
+ out.write(buffer, 0, numRead);
+ if (sizeBytes > 0) {
+ totalRead += numRead;
+ float fraction = ((float) totalRead / (float) sizeBytes);
+ session.setStagingProgress(fraction);
+ publishProgress((int) (fraction * 100.0));
+ }
+ }
+ }
+ return pi.getSessionInfo(mStagedSessionId);
+ } catch (IOException | SecurityException | IllegalStateException
+ | IllegalArgumentException e) {
+ Log.w(TAG, "Error staging apk from content URI", e);
+ return null;
+ }
+ }
+
+ private long getContentSizeBytes() {
+ try (AssetFileDescriptor afd = mContext.getContentResolver()
+ .openAssetFileDescriptor(mUri, "r")) {
+ return afd != null ? afd.getLength() : UNKNOWN_LENGTH;
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to open asset file descriptor", e);
+ return UNKNOWN_LENGTH;
+ }
+ }
+
+ public MutableLiveData<Integer> getProgress() {
+ return mProgressLiveData;
+ }
+
+ @Override
+ protected void onProgressUpdate(Integer... progress) {
+ if (progress != null && progress.length > 0) {
+ mProgressLiveData.setValue(progress[0]);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(SessionInfo sessionInfo) {
+ if (sessionInfo == null || !sessionInfo.isActive()
+ || sessionInfo.getResolvedBaseApkPath() == null) {
+ Log.w(TAG, "Session info is invalid: " + sessionInfo);
+ mListener.onStagingFailure();
+ return;
+ }
+ mListener.onStagingSuccess(sessionInfo);
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java
new file mode 100644
index 0000000..cc9857d
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java
@@ -0,0 +1,112 @@
+/*
+ * 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.packageinstaller.v2.model.installstagedata;
+
+
+import android.app.Activity;
+import android.content.Intent;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class InstallAborted extends InstallStage {
+
+ public static final int ABORT_REASON_INTERNAL_ERROR = 0;
+ public static final int ABORT_REASON_POLICY = 1;
+ private final int mStage = InstallStage.STAGE_ABORTED;
+ private final int mAbortReason;
+
+ /**
+ * It will hold the restriction name, when the restriction was enforced by the system, and not
+ * a device admin.
+ */
+ @NonNull
+ private final String mMessage;
+ /**
+ * <p>If abort reason is ABORT_REASON_POLICY, then this will hold the Intent
+ * to display a support dialog when a feature was disabled by an admin. It will be
+ * {@code null} if the feature is disabled by the system. In this case, the restriction name
+ * will be set in {@link #mMessage} </p>
+ *
+ * <p>If the abort reason is ABORT_REASON_INTERNAL_ERROR, it <b>may</b> hold an
+ * intent to be sent as a result to the calling activity.</p>
+ */
+ @Nullable
+ private final Intent mIntent;
+ private final int mActivityResultCode;
+
+ private InstallAborted(int reason, @NonNull String message, @Nullable Intent intent,
+ int activityResultCode) {
+ mAbortReason = reason;
+ mMessage = message;
+ mIntent = intent;
+ mActivityResultCode = activityResultCode;
+ }
+
+ public int getAbortReason() {
+ return mAbortReason;
+ }
+
+ @NonNull
+ public String getMessage() {
+ return mMessage;
+ }
+
+ @Nullable
+ public Intent getResultIntent() {
+ return mIntent;
+ }
+
+ public int getActivityResultCode() {
+ return mActivityResultCode;
+ }
+
+ @Override
+ public int getStageCode() {
+ return mStage;
+ }
+
+ public static class Builder {
+
+ private final int mAbortReason;
+ private String mMessage = "";
+ private Intent mIntent = null;
+ private int mActivityResultCode = Activity.RESULT_CANCELED;
+
+ public Builder(int reason) {
+ mAbortReason = reason;
+ }
+
+ public Builder setMessage(@NonNull String message) {
+ mMessage = message;
+ return this;
+ }
+
+ public Builder setResultIntent(@NonNull Intent intent) {
+ mIntent = intent;
+ return this;
+ }
+
+ public Builder setActivityResultCode(int resultCode) {
+ mActivityResultCode = resultCode;
+ return this;
+ }
+
+ public InstallAborted build() {
+ return new InstallAborted(mAbortReason, mMessage, mIntent, mActivityResultCode);
+ }
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallReady.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallReady.java
new file mode 100644
index 0000000..548f2c5
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallReady.java
@@ -0,0 +1,27 @@
+/*
+ * 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
+ *
+ * https://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.packageinstaller.v2.model.installstagedata;
+
+public class InstallReady extends InstallStage{
+
+ private final int mStage = InstallStage.STAGE_READY;
+
+ @Override
+ public int getStageCode() {
+ return mStage;
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStage.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStage.java
new file mode 100644
index 0000000..f91e64b
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStage.java
@@ -0,0 +1,34 @@
+/*
+ * 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.packageinstaller.v2.model.installstagedata;
+
+public abstract class InstallStage {
+
+ public static final int STAGE_DEFAULT = -1;
+ public static final int STAGE_ABORTED = 0;
+ public static final int STAGE_STAGING = 1;
+ public static final int STAGE_READY = 2;
+ public static final int STAGE_USER_ACTION_REQUIRED = 3;
+ public static final int STAGE_INSTALLING = 4;
+ public static final int STAGE_SUCCESS = 5;
+ public static final int STAGE_FAILED = 6;
+
+ /**
+ * @return the integer value representing current install stage.
+ */
+ public abstract int getStageCode();
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStaging.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStaging.java
new file mode 100644
index 0000000..a979cf8
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStaging.java
@@ -0,0 +1,27 @@
+/*
+ * 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.packageinstaller.v2.model.installstagedata;
+
+public class InstallStaging extends InstallStage {
+
+ private final int mStage = InstallStage.STAGE_STAGING;
+
+ @Override
+ public int getStageCode() {
+ return mStage;
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java
new file mode 100644
index 0000000..ba5a0cd
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java
@@ -0,0 +1,172 @@
+/*
+ * 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.packageinstaller.v2.ui;
+
+import static android.os.Process.INVALID_UID;
+import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_INTERNAL_ERROR;
+import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_POLICY;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.util.Log;
+import android.view.Window;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.ViewModelProvider;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.InstallRepository;
+import com.android.packageinstaller.v2.model.InstallRepository.CallerInfo;
+import com.android.packageinstaller.v2.model.installstagedata.InstallAborted;
+import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
+import com.android.packageinstaller.v2.ui.fragments.InstallStagingFragment;
+import com.android.packageinstaller.v2.ui.fragments.SimpleErrorFragment;
+import com.android.packageinstaller.v2.viewmodel.InstallViewModel;
+import com.android.packageinstaller.v2.viewmodel.InstallViewModelFactory;
+
+public class InstallLaunch extends FragmentActivity {
+
+ public static final String EXTRA_CALLING_PKG_UID =
+ InstallLaunch.class.getPackageName() + ".callingPkgUid";
+ public static final String EXTRA_CALLING_PKG_NAME =
+ InstallLaunch.class.getPackageName() + ".callingPkgName";
+ private static final String TAG = InstallLaunch.class.getSimpleName();
+ private static final String TAG_DIALOG = "dialog";
+ private final boolean mLocalLOGV = false;
+ private InstallViewModel mInstallViewModel;
+ private InstallRepository mInstallRepository;
+
+ private FragmentManager mFragmentManager;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ this.requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+ mFragmentManager = getSupportFragmentManager();
+ mInstallRepository = new InstallRepository(getApplicationContext());
+ mInstallViewModel = new ViewModelProvider(this,
+ new InstallViewModelFactory(this.getApplication(), mInstallRepository)).get(
+ InstallViewModel.class);
+
+ Intent intent = getIntent();
+ CallerInfo info = new CallerInfo(
+ intent.getStringExtra(EXTRA_CALLING_PKG_NAME),
+ intent.getIntExtra(EXTRA_CALLING_PKG_UID, INVALID_UID));
+ mInstallViewModel.preprocessIntent(intent, info);
+
+ mInstallViewModel.getCurrentInstallStage().observe(this, this::onInstallStageChange);
+ }
+
+ /**
+ * Main controller of the UI. This method shows relevant dialogs based on the install stage
+ */
+ private void onInstallStageChange(InstallStage installStage) {
+ if (installStage.getStageCode() == InstallStage.STAGE_STAGING) {
+ InstallStagingFragment stagingDialog = new InstallStagingFragment();
+ showDialogInner(stagingDialog);
+ mInstallViewModel.getStagingProgress().observe(this, stagingDialog::setProgress);
+ } else if (installStage.getStageCode() == InstallStage.STAGE_ABORTED) {
+ InstallAborted aborted = (InstallAborted) installStage;
+ switch (aborted.getAbortReason()) {
+ // TODO: check if any dialog is to be shown for ABORT_REASON_INTERNAL_ERROR
+ case ABORT_REASON_INTERNAL_ERROR -> setResult(RESULT_CANCELED, true);
+ case ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted);
+ default -> setResult(RESULT_CANCELED, true);
+ }
+ } else {
+ Log.d(TAG, "Unimplemented stage: " + installStage.getStageCode());
+ showDialogInner(null);
+ }
+ }
+
+ private void showPolicyRestrictionDialog(InstallAborted aborted) {
+ String restriction = aborted.getMessage();
+ Intent adminSupportIntent = aborted.getResultIntent();
+ boolean shouldFinish;
+
+ // If the given restriction is set by an admin, display information about the
+ // admin enforcing the restriction for the affected user. If not enforced by the admin,
+ // show the system dialog.
+ if (adminSupportIntent != null) {
+ if (mLocalLOGV) {
+ Log.i(TAG, "Restriction set by admin, starting " + adminSupportIntent);
+ }
+ startActivity(adminSupportIntent);
+ // Finish the package installer app since the next dialog will not be shown by this app
+ shouldFinish = true;
+ } else {
+ if (mLocalLOGV) {
+ Log.i(TAG, "Restriction set by system: " + restriction);
+ }
+ DialogFragment blockedByPolicyDialog = createDevicePolicyRestrictionDialog(restriction);
+ // Don't finish the package installer app since the next dialog
+ // will be shown by this app
+ shouldFinish = false;
+ showDialogInner(blockedByPolicyDialog);
+ }
+ setResult(RESULT_CANCELED, shouldFinish);
+ }
+
+ /**
+ * Create a new dialog based on the install restriction enforced.
+ *
+ * @param restriction The restriction to create the dialog for
+ * @return The dialog
+ */
+ private DialogFragment createDevicePolicyRestrictionDialog(String restriction) {
+ if (mLocalLOGV) {
+ Log.i(TAG, "createDialog(" + restriction + ")");
+ }
+ return switch (restriction) {
+ case UserManager.DISALLOW_INSTALL_APPS ->
+ new SimpleErrorFragment(R.string.install_apps_user_restriction_dlg_text);
+ case UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
+ UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY ->
+ new SimpleErrorFragment(R.string.unknown_apps_user_restriction_dlg_text);
+ default -> null;
+ };
+ }
+
+ /**
+ * Replace any visible dialog by the dialog returned by InstallRepository
+ *
+ * @param newDialog The new dialog to display
+ */
+ private void showDialogInner(@Nullable DialogFragment newDialog) {
+ DialogFragment currentDialog = (DialogFragment) mFragmentManager.findFragmentByTag(
+ TAG_DIALOG);
+ if (currentDialog != null) {
+ currentDialog.dismissAllowingStateLoss();
+ }
+ if (newDialog != null) {
+ newDialog.show(mFragmentManager, TAG_DIALOG);
+ }
+ }
+
+ public void setResult(int resultCode, boolean shouldFinish) {
+ // TODO: This is incomplete. We need to send RESULT_FIRST_USER, RESULT_OK etc
+ // for relevant use cases. Investigate when to send what result.
+ super.setResult(resultCode);
+ if (shouldFinish) {
+ finish();
+ }
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallStagingFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallStagingFragment.java
new file mode 100644
index 0000000..feb2428
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallStagingFragment.java
@@ -0,0 +1,69 @@
+/*
+ * 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.packageinstaller.v2.ui.fragments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ProgressBar;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+
+public class InstallStagingFragment extends DialogFragment {
+
+ private static final String TAG = InstallStagingFragment.class.getSimpleName();
+ private ProgressBar mProgressBar;
+ private AlertDialog mDialog;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null);
+ dialogView.requireViewById(R.id.staging).setVisibility(View.VISIBLE);
+
+ mDialog = new AlertDialog.Builder(requireContext())
+ .setTitle(getString(R.string.app_name_unknown))
+ .setIcon(R.drawable.ic_file_download)
+ .setView(dialogView)
+ .setNegativeButton(R.string.cancel, null)
+ .setCancelable(false)
+ .create();
+
+ mDialog.setCanceledOnTouchOutside(false);
+ return mDialog;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setEnabled(false);
+ mProgressBar = mDialog.requireViewById(R.id.progress_indeterminate);
+ mProgressBar.setProgress(0);
+ mProgressBar.setMax(100);
+ mProgressBar.setIndeterminate(false);
+ }
+
+ public void setProgress(int progress) {
+ if (mProgressBar != null) {
+ mProgressBar.setProgress(progress);
+ }
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java
new file mode 100644
index 0000000..dce0b9a
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java
@@ -0,0 +1,51 @@
+/*
+ * 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.packageinstaller.v2.ui.fragments;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+
+public class SimpleErrorFragment extends DialogFragment {
+
+ private static final String TAG = SimpleErrorFragment.class.getSimpleName();
+ private final int mMessageResId;
+
+ public SimpleErrorFragment(int messageResId) {
+ mMessageResId = messageResId;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return new AlertDialog.Builder(getActivity())
+ .setMessage(mMessageResId)
+ .setPositiveButton(R.string.ok, (dialog, which) -> getActivity().finish())
+ .create();
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ getActivity().setResult(Activity.RESULT_CANCELED);
+ getActivity().finish();
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java
new file mode 100644
index 0000000..42b3023
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java
@@ -0,0 +1,70 @@
+/*
+ * 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.packageinstaller.v2.viewmodel;
+
+import android.app.Application;
+import android.content.Intent;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.MutableLiveData;
+import com.android.packageinstaller.v2.model.InstallRepository;
+import com.android.packageinstaller.v2.model.InstallRepository.CallerInfo;
+import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
+import com.android.packageinstaller.v2.model.installstagedata.InstallStaging;
+
+
+public class InstallViewModel extends AndroidViewModel {
+
+ private static final String TAG = InstallViewModel.class.getSimpleName();
+ private final InstallRepository mRepository;
+ private final MediatorLiveData<InstallStage> mCurrentInstallStage = new MediatorLiveData<>(
+ new InstallStaging());
+
+ public InstallViewModel(@NonNull Application application, InstallRepository repository) {
+ super(application);
+ mRepository = repository;
+ }
+
+ public MutableLiveData<InstallStage> getCurrentInstallStage() {
+ return mCurrentInstallStage;
+ }
+
+ public void preprocessIntent(Intent intent, CallerInfo callerInfo) {
+ InstallStage stage = mRepository.performPreInstallChecks(intent, callerInfo);
+ if (stage.getStageCode() == InstallStage.STAGE_ABORTED) {
+ mCurrentInstallStage.setValue(stage);
+ } else {
+ // Since staging is an async operation, we will get the staging result later in time.
+ // Result of the file staging will be set in InstallRepository#mStagingResult.
+ // As such, mCurrentInstallStage will need to add another MutableLiveData
+ // as a data source
+ mRepository.stageForInstall();
+ mCurrentInstallStage.addSource(mRepository.getStagingResult(), installStage -> {
+ if (installStage.getStageCode() != InstallStage.STAGE_READY) {
+ mCurrentInstallStage.setValue(installStage);
+ } else {
+ // Proceed with user confirmation here.
+ }
+ });
+ }
+ }
+
+ public MutableLiveData<Integer> getStagingProgress() {
+ return mRepository.getStagingProgress();
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.java
new file mode 100644
index 0000000..ef459e6
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.java
@@ -0,0 +1,45 @@
+/*
+ * 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.packageinstaller.v2.viewmodel;
+
+import android.app.Application;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+import com.android.packageinstaller.v2.model.InstallRepository;
+
+public class InstallViewModelFactory extends ViewModelProvider.AndroidViewModelFactory {
+
+ private final InstallRepository mRepository;
+ private final Application mApplication;
+
+ public InstallViewModelFactory(Application application, InstallRepository repository) {
+ // Calling super class' ctor ensures that create method is called correctly and the right
+ // ctor of InstallViewModel is used. If we fail to do that, the default ctor:
+ // InstallViewModel(application) is used, and repository isn't initialized in the viewmodel
+ super(application);
+ mApplication = application;
+ mRepository = repository;
+ }
+
+ @NonNull
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+ return (T) new InstallViewModel(mApplication, mRepository);
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index fa8c1fb..0ffcc45 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -594,6 +594,16 @@
|| cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO);
}
+ /**
+ * Check if the Bluetooth device is an active LE Audio device
+ *
+ * @param cachedDevice the CachedBluetoothDevice
+ * @return if the Bluetooth device is an active LE Audio device
+ */
+ public static boolean isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice) {
+ return cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO);
+ }
+
private static boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) {
if (cachedDevice == null) {
return false;
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
index 53e437a..0b13383 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
@@ -28,6 +28,7 @@
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -63,11 +64,13 @@
val isImeVisible by rememberUpdatedState(WindowInsets.imeAnimationTarget.getBottom(density) > 0)
LaunchedEffect(isImeVisible) { viewModel.onImeVisibilityChanged(isImeVisible) }
- LaunchedEffect(Unit) {
+ DisposableEffect(Unit) {
+ viewModel.onShown()
+
// When the UI comes up, request focus on the TextField to bring up the software keyboard.
focusRequester.requestFocus()
- // Also, report that the UI is shown to let the view-model run some logic.
- viewModel.onShown()
+
+ onDispose { viewModel.onHidden() }
}
LaunchedEffect(animateFailure) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 03efbe0..2bbe9b8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -26,6 +26,7 @@
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -65,8 +66,10 @@
viewModel: PatternBouncerViewModel,
modifier: Modifier = Modifier,
) {
- // Report that the UI is shown to let the view-model run some logic.
- LaunchedEffect(Unit) { viewModel.onShown() }
+ DisposableEffect(Unit) {
+ viewModel.onShown()
+ onDispose { viewModel.onHidden() }
+ }
val colCount = viewModel.columnCount
val rowCount = viewModel.rowCount
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index 243751fa..59617c9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -31,6 +31,7 @@
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -69,8 +70,10 @@
viewModel: PinBouncerViewModel,
modifier: Modifier = Modifier,
) {
- // Report that the UI is shown to let the view-model run some logic.
- LaunchedEffect(Unit) { viewModel.onShown() }
+ DisposableEffect(Unit) {
+ viewModel.onShown()
+ onDispose { viewModel.onHidden() }
+ }
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState()
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/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 4175937..b064391 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -83,6 +83,7 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter;
import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels;
import com.android.systemui.log.SessionTracker;
@@ -170,6 +171,7 @@
@NonNull private final SelectedUserInteractor mSelectedUserInteractor;
@NonNull private final FpsUnlockTracker mFpsUnlockTracker;
private final boolean mIgnoreRefreshRate;
+ private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
// Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple
// sensors, this, in addition to a lot of the code here, will be updated.
@@ -283,8 +285,8 @@
mPrimaryBouncerInteractor,
mAlternateBouncerInteractor,
mUdfpsKeyguardAccessibilityDelegate,
- mUdfpsKeyguardViewModels,
- mSelectedUserInteractor
+ mKeyguardTransitionInteractor,
+ mSelectedUserInteractor
)));
}
@@ -649,7 +651,8 @@
@NonNull UdfpsKeyguardAccessibilityDelegate udfpsKeyguardAccessibilityDelegate,
@NonNull Provider<UdfpsKeyguardViewModels> udfpsKeyguardViewModelsProvider,
@NonNull SelectedUserInteractor selectedUserInteractor,
- @NonNull FpsUnlockTracker fpsUnlockTracker) {
+ @NonNull FpsUnlockTracker fpsUnlockTracker,
+ @NonNull KeyguardTransitionInteractor keyguardTransitionInteractor) {
mContext = context;
mExecution = execution;
mVibrator = vibrator;
@@ -695,6 +698,7 @@
mSelectedUserInteractor = selectedUserInteractor;
mFpsUnlockTracker = fpsUnlockTracker;
mFpsUnlockTracker.startTracking();
+ mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mTouchProcessor = singlePointerTouchProcessor;
mSessionTracker = sessionTracker;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 934f9f9..8f31a2d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -51,8 +51,8 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.res.R
import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -63,7 +63,6 @@
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import javax.inject.Provider
private const val TAG = "UdfpsControllerOverlay"
@@ -102,7 +101,7 @@
private val alternateBouncerInteractor: AlternateBouncerInteractor,
private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
- private val udfpsKeyguardViewModels: Provider<UdfpsKeyguardViewModels>,
+ private val transitionInteractor: KeyguardTransitionInteractor,
private val selectedUserInteractor: SelectedUserInteractor,
) {
/** The view, when [isShowing], or null. */
@@ -264,11 +263,11 @@
dialogManager,
controller,
activityLaunchAnimator,
- featureFlags,
primaryBouncerInteractor,
alternateBouncerInteractor,
udfpsKeyguardAccessibilityDelegate,
selectedUserInteractor,
+ transitionInteractor,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
index d7df0e5..2c4ed58 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
@@ -16,7 +16,6 @@
package com.android.systemui.biometrics
-import android.animation.ValueAnimator
import android.content.res.Configuration
import android.util.MathUtils
import android.view.View
@@ -27,17 +26,17 @@
import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.biometrics.UdfpsKeyguardViewLegacy.ANIMATION_UNLOCKED_SCREEN_OFF
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.res.R
import com.android.systemui.statusbar.LockscreenShadeTransitionController
import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.KeyguardViewManagerCallback
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.OccludingAppBiometricUI
@@ -48,10 +47,14 @@
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import java.io.PrintWriter
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
/** Class that coordinates non-HBM animations during keyguard authentication. */
+@ExperimentalCoroutinesApi
open class UdfpsKeyguardViewControllerLegacy(
private val view: UdfpsKeyguardViewLegacy,
statusBarStateController: StatusBarStateController,
@@ -65,11 +68,11 @@
systemUIDialogManager: SystemUIDialogManager,
private val udfpsController: UdfpsController,
private val activityLaunchAnimator: ActivityLaunchAnimator,
- featureFlags: FeatureFlags,
primaryBouncerInteractor: PrimaryBouncerInteractor,
private val alternateBouncerInteractor: AlternateBouncerInteractor,
private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
private val selectedUserInteractor: SelectedUserInteractor,
+ private val transitionInteractor: KeyguardTransitionInteractor,
) :
UdfpsAnimationViewController<UdfpsKeyguardViewLegacy>(
view,
@@ -91,44 +94,10 @@
private var launchTransitionFadingAway = false
private var isLaunchingActivity = false
private var activityLaunchProgress = 0f
- private val unlockedScreenOffDozeAnimator =
- ValueAnimator.ofFloat(0f, 1f).apply {
- duration = StackStateAnimator.ANIMATION_DURATION_STANDARD.toLong()
- interpolator = Interpolators.ALPHA_IN
- addUpdateListener { animation ->
- view.onDozeAmountChanged(
- animation.animatedFraction,
- animation.animatedValue as Float,
- UdfpsKeyguardViewLegacy.ANIMATION_UNLOCKED_SCREEN_OFF
- )
- }
- }
private var inputBouncerExpansion = 0f
private val stateListener: StatusBarStateController.StateListener =
object : StatusBarStateController.StateListener {
- override fun onDozeAmountChanged(linear: Float, eased: Float) {
- if (lastDozeAmount < linear) {
- showUdfpsBouncer(false)
- }
- unlockedScreenOffDozeAnimator.cancel()
- val animatingFromUnlockedScreenOff =
- unlockedScreenOffAnimationController.isAnimationPlaying()
- if (animatingFromUnlockedScreenOff && linear != 0f) {
- // we manually animate the fade in of the UDFPS icon since the unlocked
- // screen off animation prevents the doze amounts to be incrementally eased in
- unlockedScreenOffDozeAnimator.start()
- } else {
- view.onDozeAmountChanged(
- linear,
- eased,
- UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN
- )
- }
- lastDozeAmount = linear
- updatePauseAuth()
- }
-
override fun onStateChanged(statusBarState: Int) {
this@UdfpsKeyguardViewControllerLegacy.statusBarState = statusBarState
updateAlpha()
@@ -222,11 +191,39 @@
repeatOnLifecycle(Lifecycle.State.CREATED) {
listenForBouncerExpansion(this)
listenForAlternateBouncerVisibility(this)
+ listenForGoneToAodTransition(this)
+ listenForLockscreenAodTransitions(this)
}
}
}
@VisibleForTesting
+ suspend fun listenForGoneToAodTransition(scope: CoroutineScope): Job {
+ return scope.launch {
+ transitionInteractor.goneToAodTransition.collect { transitionStep ->
+ view.onDozeAmountChanged(
+ transitionStep.value,
+ transitionStep.value,
+ ANIMATION_UNLOCKED_SCREEN_OFF,
+ )
+ }
+ }
+ }
+
+ @VisibleForTesting
+ suspend fun listenForLockscreenAodTransitions(scope: CoroutineScope): Job {
+ return scope.launch {
+ transitionInteractor.dozeAmountTransition.collect { transitionStep ->
+ view.onDozeAmountChanged(
+ transitionStep.value,
+ transitionStep.value,
+ UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN,
+ )
+ }
+ }
+ }
+
+ @VisibleForTesting
override suspend fun listenForBouncerExpansion(scope: CoroutineScope): Job {
return scope.launch {
primaryBouncerInteractor.bouncerExpansion.collect { bouncerExpansion: Float ->
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java
index 95e3a76..f4ed8ce 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java
@@ -73,9 +73,6 @@
// AOD anti-burn-in offsets
private final int mMaxBurnInOffsetX;
private final int mMaxBurnInOffsetY;
- private float mBurnInOffsetX;
- private float mBurnInOffsetY;
- private float mBurnInProgress;
private float mInterpolatedDarkAmount;
private int mAnimationType = ANIMATION_NONE;
private boolean mFullyInflated;
@@ -138,20 +135,22 @@
// AoD-burn in location, else we need to translate the icon from LS => AoD.
final float darkAmountForAnimation = mAnimationType == ANIMATION_UNLOCKED_SCREEN_OFF
? 1f : mInterpolatedDarkAmount;
- mBurnInOffsetX = MathUtils.lerp(0f,
+ final float burnInOffsetX = MathUtils.lerp(0f,
getBurnInOffset(mMaxBurnInOffsetX * 2, true /* xAxis */)
- mMaxBurnInOffsetX, darkAmountForAnimation);
- mBurnInOffsetY = MathUtils.lerp(0f,
+ final float burnInOffsetY = MathUtils.lerp(0f,
getBurnInOffset(mMaxBurnInOffsetY * 2, false /* xAxis */)
- mMaxBurnInOffsetY, darkAmountForAnimation);
- mBurnInProgress = MathUtils.lerp(0f, getBurnInProgressOffset(), darkAmountForAnimation);
+ final float burnInProgress = MathUtils.lerp(0f, getBurnInProgressOffset(),
+ darkAmountForAnimation);
if (mAnimationType == ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN && !mPauseAuth) {
- mLockScreenFp.setTranslationX(mBurnInOffsetX);
- mLockScreenFp.setTranslationY(mBurnInOffsetY);
+ mLockScreenFp.setTranslationX(burnInOffsetX);
+ mLockScreenFp.setTranslationY(burnInOffsetY);
mBgProtection.setAlpha(1f - mInterpolatedDarkAmount);
mLockScreenFp.setAlpha(1f - mInterpolatedDarkAmount);
} else if (darkAmountForAnimation == 0f) {
+ // we're on the lockscreen and should use mAlpha (changes based on shade expansion)
mLockScreenFp.setTranslationX(0);
mLockScreenFp.setTranslationY(0);
mBgProtection.setAlpha(mAlpha / 255f);
@@ -162,9 +161,9 @@
}
mLockScreenFp.setProgress(1f - mInterpolatedDarkAmount);
- mAodFp.setTranslationX(mBurnInOffsetX);
- mAodFp.setTranslationY(mBurnInOffsetY);
- mAodFp.setProgress(mBurnInProgress);
+ mAodFp.setTranslationX(burnInOffsetX);
+ mAodFp.setTranslationY(burnInOffsetY);
+ mAodFp.setProgress(burnInProgress);
mAodFp.setAlpha(mInterpolatedDarkAmount);
// done animating
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index f46574c..8024874 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -62,6 +62,13 @@
/** Notifies that the UI has been shown to the user. */
fun onShown() {
+ interactor.resetMessage()
+ }
+
+ /**
+ * Notifies that the UI has been hidden from the user (after any transitions have completed).
+ */
+ fun onHidden() {
clearInput()
interactor.resetMessage()
}
@@ -113,8 +120,6 @@
}
_animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED
- // TODO(b/291528545): On success, this should only be cleared after the view is animated
- // away).
clearInput()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index f93efa1..5f54a98 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -130,6 +130,7 @@
import com.android.systemui.unfold.SysUIUnfoldModule;
import com.android.systemui.user.UserModule;
import com.android.systemui.user.domain.UserDomainLayerModule;
+import com.android.systemui.util.EventLogModule;
import com.android.systemui.util.concurrency.SysUIConcurrencyModule;
import com.android.systemui.util.dagger.UtilModule;
import com.android.systemui.util.kotlin.CoroutinesModule;
@@ -186,6 +187,7 @@
DisableFlagsModule.class,
DisplayModule.class,
DreamModule.class,
+ EventLogModule.class,
FalsingModule.class,
FlagsModule.class,
FlagDependenciesModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index 78f2da5..789a1e4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -279,6 +279,11 @@
}
@Override
+ public void onNullBinding(ComponentName name) {
+ executeSetBindService(false);
+ }
+
+ @Override
public void onServiceDisconnected(ComponentName name) {
if (DEBUG) Log.d(TAG, "onServiceDisconnected " + name);
handleDeath();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
index 9ff416a..9f2b0a6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
@@ -32,6 +32,7 @@
import com.android.systemui.statusbar.StatusBarState.SHADE
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.MAX_HUN_WHEN_AGE_MS
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.HUN_SUPPRESSED_OLD_WHEN
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
@@ -141,7 +142,11 @@
}
class PeekOldWhenSuppressor(private val systemClock: SystemClock) :
- VisualInterruptionFilter(types = setOf(PEEK), reason = "has old `when`") {
+ VisualInterruptionFilter(
+ types = setOf(PEEK),
+ reason = "has old `when`",
+ uiEventId = HUN_SUPPRESSED_OLD_WHEN
+ ) {
private fun whenAge(entry: NotificationEntry) =
systemClock.currentTimeMillis() - entry.sbn.notification.`when`
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt
index b44a367..2707ed8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt
@@ -18,6 +18,7 @@
import android.app.NotificationManager.IMPORTANCE_HIGH
import android.os.PowerManager
+import com.android.internal.logging.UiEventLogger.UiEventEnum
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.StatusBarState.KEYGUARD
import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -37,6 +38,10 @@
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSED_ONLY_BY_DND
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSIVE_BUBBLE_METADATA
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionSuppressor.EventLogData
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -52,6 +57,8 @@
val logReason: String
val shouldLog: Boolean
val isWarning: Boolean
+ val uiEventId: UiEventEnum?
+ val eventLogData: EventLogData?
}
private enum class DecisionImpl(
@@ -60,7 +67,9 @@
override val wouldFsiWithoutDnd: Boolean = shouldFsi,
val supersedesDnd: Boolean = false,
override val shouldLog: Boolean = true,
- override val isWarning: Boolean = false
+ override val isWarning: Boolean = false,
+ override val uiEventId: UiEventEnum? = null,
+ override val eventLogData: EventLogData? = null
) : Decision {
NO_FSI_NO_FULL_SCREEN_INTENT(
false,
@@ -73,9 +82,17 @@
NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(
false,
"suppressive group alert behavior",
- isWarning = true
+ isWarning = true,
+ uiEventId = FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR,
+ eventLogData = EventLogData("231322873", "groupAlertBehavior")
),
- NO_FSI_SUPPRESSIVE_BUBBLE_METADATA(false, "suppressive bubble metadata", isWarning = true),
+ NO_FSI_SUPPRESSIVE_BUBBLE_METADATA(
+ false,
+ "suppressive bubble metadata",
+ isWarning = true,
+ uiEventId = FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA,
+ eventLogData = EventLogData("274759612", "bubbleMetadata")
+ ),
NO_FSI_PACKAGE_SUSPENDED(false, "package suspended"),
FSI_DEVICE_NOT_INTERACTIVE(true, "device is not interactive"),
FSI_DEVICE_DREAMING(true, "device is dreaming"),
@@ -84,7 +101,13 @@
FSI_KEYGUARD_OCCLUDED(true, "keyguard is occluded"),
FSI_LOCKED_SHADE(true, "locked shade"),
FSI_DEVICE_NOT_PROVISIONED(true, "device not provisioned"),
- NO_FSI_NO_HUN_OR_KEYGUARD(false, "no HUN or keyguard", isWarning = true),
+ NO_FSI_NO_HUN_OR_KEYGUARD(
+ false,
+ "no HUN or keyguard",
+ isWarning = true,
+ uiEventId = FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD,
+ eventLogData = EventLogData("231322873", "no hun or keyguard")
+ ),
NO_FSI_SUPPRESSED_BY_DND(false, "suppressed by DND", wouldFsiWithoutDnd = false),
NO_FSI_SUPPRESSED_ONLY_BY_DND(false, "suppressed only by DND", wouldFsiWithoutDnd = true)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index f2ade34..4045380 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -49,6 +49,7 @@
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.EventLog;
import com.android.systemui.util.settings.GlobalSettings;
import com.android.systemui.util.time.SystemClock;
@@ -81,6 +82,7 @@
private final DeviceProvisionedController mDeviceProvisionedController;
private final SystemClock mSystemClock;
private final GlobalSettings mGlobalSettings;
+ private final EventLog mEventLog;
@VisibleForTesting
protected boolean mUseHeadsUp = false;
@@ -129,7 +131,8 @@
UserTracker userTracker,
DeviceProvisionedController deviceProvisionedController,
SystemClock systemClock,
- GlobalSettings globalSettings) {
+ GlobalSettings globalSettings,
+ EventLog eventLog) {
mPowerManager = powerManager;
mBatteryController = batteryController;
mAmbientDisplayConfiguration = ambientDisplayConfiguration;
@@ -144,6 +147,7 @@
mDeviceProvisionedController = deviceProvisionedController;
mSystemClock = systemClock;
mGlobalSettings = globalSettings;
+ mEventLog = eventLog;
ContentObserver headsUpObserver = new ContentObserver(mainHandler) {
@Override
public void onChange(boolean selfChange) {
@@ -369,7 +373,7 @@
// explicitly prevent logging for this (frequent) case
return;
case NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR:
- android.util.EventLog.writeEvent(0x534e4554, "231322873", uid,
+ mEventLog.writeEvent(0x534e4554, "231322873", uid,
"groupAlertBehavior");
mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR, uid,
packageName);
@@ -377,7 +381,7 @@
decision + ": GroupAlertBehavior will prevent HUN");
return;
case NO_FSI_SUPPRESSIVE_BUBBLE_METADATA:
- android.util.EventLog.writeEvent(0x534e4554, "274759612", uid,
+ mEventLog.writeEvent(0x534e4554, "274759612", uid,
"bubbleMetadata");
mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA, uid,
packageName);
@@ -385,7 +389,7 @@
decision + ": BubbleMetadata may prevent HUN");
return;
case NO_FSI_NO_HUN_OR_KEYGUARD:
- android.util.EventLog.writeEvent(0x534e4554, "231322873", uid,
+ mEventLog.writeEvent(0x534e4554, "231322873", uid,
"no hun or keyguard");
mUiEventLogger.log(FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD, uid, packageName);
mLogger.logNoFullscreenWarning(entry,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt
index d7f0baf..f732e8d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt
@@ -17,6 +17,7 @@
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.RefactorFlagUtils
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.Decision
@@ -62,6 +63,21 @@
wrapped.removeSuppressor(suppressor)
}
+ override fun addCondition(condition: VisualInterruptionCondition) = notValidInLegacyMode()
+
+ override fun removeCondition(condition: VisualInterruptionCondition) = notValidInLegacyMode()
+
+ override fun addFilter(filter: VisualInterruptionFilter) = notValidInLegacyMode()
+
+ override fun removeFilter(filter: VisualInterruptionFilter) = notValidInLegacyMode()
+
+ private fun notValidInLegacyMode() {
+ RefactorFlagUtils.assertOnEngBuild(
+ "This method is only implemented in VisualInterruptionDecisionProviderImpl, " +
+ "and so should only be called when FLAG_VISUAL_INTERRUPTIONS_REFACTOR is enabled."
+ )
+ }
+
override fun makeUnloggedHeadsUpDecision(entry: NotificationEntry): Decision =
wrapped.checkHeadsUp(entry, /* log= */ false).let { DecisionImpl.of(it) }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt
index da8474e..de8863c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.interruption
+import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.statusbar.notification.collection.NotificationEntry
/**
@@ -51,33 +52,77 @@
val wouldInterruptWithoutDnd: Boolean
}
- /**
- * Initializes the provider.
- *
- * Must be called before any method except [addLegacySuppressor].
- */
+ /** Initializes the provider. */
fun start() {}
/**
- * Adds a [component][suppressor] that can suppress visual interruptions.
+ * Adds a [NotificationInterruptSuppressor] that can suppress visual interruptions.
*
- * This class may call suppressors in any order.
+ * This method may be called before [start] has been called.
+ *
+ * This class may call suppressors, conditions, and filters in any order.
*
* @param[suppressor] the suppressor to add
*/
fun addLegacySuppressor(suppressor: NotificationInterruptSuppressor)
/**
- * Removes a [component][suppressor] that can suppress visual interruptions.
+ * Removes a previously-added suppressor.
+ *
+ * This method may be called before [start] has been called.
*
* @param[suppressor] the suppressor to remove
*/
- fun removeLegacySuppressor(suppressor: NotificationInterruptSuppressor)
+ @VisibleForTesting fun removeLegacySuppressor(suppressor: NotificationInterruptSuppressor)
+
+ /**
+ * Adds a [VisualInterruptionCondition] that can suppress visual interruptions without examining
+ * individual notifications.
+ *
+ * This method may be called before [start] has been called.
+ *
+ * This class may call suppressors, conditions, and filters in any order.
+ *
+ * @param[condition] the condition to add
+ */
+ fun addCondition(condition: VisualInterruptionCondition)
+
+ /**
+ * Removes a previously-added condition.
+ *
+ * This method may be called before [start] has been called.
+ *
+ * @param[condition] the condition to remove
+ */
+ @VisibleForTesting fun removeCondition(condition: VisualInterruptionCondition)
+
+ /**
+ * Adds a [VisualInterruptionFilter] that can suppress visual interruptions based on individual
+ * notifications.
+ *
+ * This method may be called before [start] has been called.
+ *
+ * This class may call suppressors, conditions, and filters in any order.
+ *
+ * @param[filter] the filter to add
+ */
+ fun addFilter(filter: VisualInterruptionFilter)
+
+ /**
+ * Removes a previously-added filter.
+ *
+ * This method may be called before [start] has been called.
+ *
+ * @param[filter] the filter to remove
+ */
+ @VisibleForTesting fun removeFilter(filter: VisualInterruptionFilter)
/**
* Decides whether a [notification][entry] should display as heads-up or not, but does not log
* that decision.
*
+ * [start] must be called before this method can be called.
+ *
* @param[entry] the notification that this decision is about
* @return the decision to display that notification as heads-up or not
*/
@@ -93,6 +138,8 @@
* If the device is dozing, the decision will consider whether the notification should "pulse"
* (wake the screen up and display the ambient view of the notification).
*
+ * [start] must be called before this method can be called.
+ *
* @see[makeUnloggedHeadsUpDecision]
*
* @param[entry] the notification that this decision is about
@@ -106,6 +153,8 @@
*
* The returned decision can be logged by passing it to [logFullScreenIntentDecision].
*
+ * [start] must be called before this method can be called.
+ *
* @see[makeAndLogHeadsUpDecision]
*
* @param[entry] the notification that this decision is about
@@ -116,6 +165,8 @@
/**
* Logs a previous [decision] to launch a full-screen intent or not.
*
+ * [start] must be called before this method can be called.
+ *
* @param[decision] the decision to log
*/
fun logFullScreenIntentDecision(decision: FullScreenIntentDecision)
@@ -123,6 +174,8 @@
/**
* Decides whether a [notification][entry] should display as a bubble or not.
*
+ * [start] must be called before this method can be called.
+ *
* @param[entry] the notification that this decision is about
* @return the decision to display that notification as a bubble or not
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
index c0a1a32..2b6e1a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
@@ -20,12 +20,15 @@
import android.os.PowerManager
import android.util.Log
import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.UiEventLogger.UiEventEnum
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.Decision
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.FullScreenIntentDecision
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionSuppressor.EventLogData
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
@@ -33,6 +36,7 @@
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.EventLog
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
@@ -43,6 +47,7 @@
private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
private val batteryController: BatteryController,
deviceProvisionedController: DeviceProvisionedController,
+ private val eventLog: EventLog,
private val globalSettings: GlobalSettings,
private val headsUpManager: HeadsUpManager,
private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
@@ -52,14 +57,25 @@
private val powerManager: PowerManager,
private val statusBarStateController: StatusBarStateController,
private val systemClock: SystemClock,
+ private val uiEventLogger: UiEventLogger,
private val userTracker: UserTracker,
) : VisualInterruptionDecisionProvider {
+ interface Loggable {
+ val uiEventId: UiEventEnum?
+ val eventLogData: EventLogData?
+ }
+
private class DecisionImpl(
override val shouldInterrupt: Boolean,
override val logReason: String
) : Decision
- private data class LoggableDecision private constructor(val decision: DecisionImpl) {
+ private data class LoggableDecision
+ private constructor(
+ val decision: DecisionImpl,
+ override val uiEventId: UiEventEnum? = null,
+ override val eventLogData: EventLogData? = null
+ ) : Loggable {
companion object {
val unsuppressed =
LoggableDecision(DecisionImpl(shouldInterrupt = true, logReason = "not suppressed"))
@@ -74,7 +90,9 @@
fun suppressed(suppressor: VisualInterruptionSuppressor) =
LoggableDecision(
- DecisionImpl(shouldInterrupt = false, logReason = suppressor.reason)
+ DecisionImpl(shouldInterrupt = false, logReason = suppressor.reason),
+ uiEventId = suppressor.uiEventId,
+ eventLogData = suppressor.eventLogData
)
}
}
@@ -82,7 +100,7 @@
private class FullScreenIntentDecisionImpl(
val entry: NotificationEntry,
private val fsiDecision: FullScreenIntentDecisionProvider.Decision
- ) : FullScreenIntentDecision {
+ ) : FullScreenIntentDecision, Loggable {
var hasBeenLogged = false
override val shouldInterrupt
@@ -99,6 +117,12 @@
val isWarning
get() = fsiDecision.isWarning
+
+ override val uiEventId
+ get() = fsiDecision.uiEventId
+
+ override val eventLogData
+ get() = fsiDecision.eventLogData
}
private val fullScreenIntentDecisionProvider =
@@ -147,23 +171,23 @@
legacySuppressors.remove(suppressor)
}
- fun addCondition(condition: VisualInterruptionCondition) {
+ override fun addCondition(condition: VisualInterruptionCondition) {
conditions.add(condition)
condition.start()
}
@VisibleForTesting
- fun removeCondition(condition: VisualInterruptionCondition) {
+ override fun removeCondition(condition: VisualInterruptionCondition) {
conditions.remove(condition)
}
- fun addFilter(filter: VisualInterruptionFilter) {
+ override fun addFilter(filter: VisualInterruptionFilter) {
filters.add(filter)
filter.start()
}
@VisibleForTesting
- fun removeFilter(filter: VisualInterruptionFilter) {
+ override fun removeFilter(filter: VisualInterruptionFilter) {
filters.remove(filter)
}
@@ -214,9 +238,10 @@
private fun logDecision(
type: VisualInterruptionType,
entry: NotificationEntry,
- loggable: LoggableDecision
+ loggableDecision: LoggableDecision
) {
- logger.logDecision(type.name, entry, loggable.decision)
+ logger.logDecision(type.name, entry, loggableDecision.decision)
+ logEvents(entry, loggableDecision)
}
override fun makeUnloggedFullScreenIntentDecision(
@@ -250,6 +275,14 @@
}
logger.logFullScreenIntentDecision(decision.entry, decision, decision.isWarning)
+ logEvents(decision.entry, decision)
+ }
+
+ private fun logEvents(entry: NotificationEntry, loggable: Loggable) {
+ loggable.uiEventId?.let { uiEventLogger.log(it, entry.sbn.uid, entry.sbn.packageName) }
+ loggable.eventLogData?.let {
+ eventLog.writeEvent(0x534e4554, it.number, entry.sbn.uid, it.description)
+ }
}
private fun checkSuppressInterruptions(entry: NotificationEntry) =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt
index 39199df..2047c62 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt
@@ -18,6 +18,7 @@
import com.android.internal.logging.UiEventLogger.UiEventEnum
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionSuppressor.EventLogData
/**
* A reason why visual interruptions might be suppressed.
@@ -43,6 +44,9 @@
* @see VisualInterruptionFilter
*/
sealed interface VisualInterruptionSuppressor {
+ /** Data to be logged in the EventLog when an interruption is suppressed. */
+ data class EventLogData(val number: String, val description: String)
+
/** The type(s) of interruption that this suppresses. */
val types: Set<VisualInterruptionType>
@@ -52,6 +56,9 @@
/** An optional UiEvent ID to be recorded when this suppresses an interruption. */
val uiEventId: UiEventEnum?
+ /** Optional data to be logged in the EventLog when this suppresses an interruption. */
+ val eventLogData: EventLogData?
+
/**
* Called after the suppressor is added to the [VisualInterruptionDecisionProvider] but before
* any other methods are called on the suppressor.
@@ -63,7 +70,8 @@
abstract class VisualInterruptionCondition(
override val types: Set<VisualInterruptionType>,
override val reason: String,
- override val uiEventId: UiEventEnum? = null
+ override val uiEventId: UiEventEnum? = null,
+ override val eventLogData: EventLogData? = null
) : VisualInterruptionSuppressor {
/** @return true if these interruptions should be suppressed right now. */
abstract fun shouldSuppress(): Boolean
@@ -73,7 +81,8 @@
abstract class VisualInterruptionFilter(
override val types: Set<VisualInterruptionType>,
override val reason: String,
- override val uiEventId: UiEventEnum? = null
+ override val uiEventId: UiEventEnum? = null,
+ override val eventLogData: EventLogData? = null
) : VisualInterruptionSuppressor {
/**
* @param entry the notification to consider suppressing
diff --git a/packages/SystemUI/src/com/android/systemui/util/EventLog.kt b/packages/SystemUI/src/com/android/systemui/util/EventLog.kt
new file mode 100644
index 0000000..dc794cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/EventLog.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.systemui.util
+
+/**
+ * Testable wrapper around {@link android.util.EventLog}.
+ *
+ * Dagger can inject this wrapper into your classes. The implementation just proxies calls to the
+ * real EventLog.
+ *
+ * In tests, pass an instance of FakeEventLog, which allows you to examine the values passed to the
+ * various methods below.
+ */
+interface EventLog {
+ /** @see android.util.EventLog.writeEvent */
+ fun writeEvent(tag: Int, value: Int): Int
+
+ /** @see android.util.EventLog.writeEvent */
+ fun writeEvent(tag: Int, value: Long): Int
+
+ /** @see android.util.EventLog.writeEvent */
+ fun writeEvent(tag: Int, value: Float): Int
+
+ /** @see android.util.EventLog.writeEvent */
+ fun writeEvent(tag: Int, value: String): Int
+
+ /** @see android.util.EventLog.writeEvent */
+ fun writeEvent(tag: Int, vararg values: Any): Int
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/EventLogImpl.kt b/packages/SystemUI/src/com/android/systemui/util/EventLogImpl.kt
new file mode 100644
index 0000000..6fb1adc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/EventLogImpl.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.systemui.util
+
+import javax.inject.Inject
+
+/** Default implementation of [com.android.systemui.util.EventLog]. */
+class EventLogImpl @Inject constructor() : EventLog {
+ override fun writeEvent(tag: Int, value: Int): Int =
+ android.util.EventLog.writeEvent(tag, value)
+
+ override fun writeEvent(tag: Int, value: Long): Int =
+ android.util.EventLog.writeEvent(tag, value)
+
+ override fun writeEvent(tag: Int, value: Float): Int =
+ android.util.EventLog.writeEvent(tag, value)
+
+ override fun writeEvent(tag: Int, value: String): Int =
+ android.util.EventLog.writeEvent(tag, value)
+
+ override fun writeEvent(tag: Int, vararg values: Any): Int =
+ android.util.EventLog.writeEvent(tag, *values)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/EventLogModule.kt b/packages/SystemUI/src/com/android/systemui/util/EventLogModule.kt
new file mode 100644
index 0000000..ca0876c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/EventLogModule.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.systemui.util
+
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface EventLogModule {
+ @SysUISingleton @Binds fun bindEventLog(eventLogImpl: EventLogImpl?): EventLog?
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index c5f16aa..5f0d4d4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -43,7 +43,7 @@
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.res.R
import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -68,7 +68,6 @@
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
-import javax.inject.Provider
import org.mockito.Mockito.`when` as whenever
private const val REQUEST_ID = 2L
@@ -111,11 +110,10 @@
@Mock private lateinit var featureFlags: FeatureFlags
@Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
@Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
- @Mock private lateinit var udfpsUtils: UdfpsUtils
@Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
@Mock private lateinit var udfpsKeyguardAccessibilityDelegate:
UdfpsKeyguardAccessibilityDelegate
- @Mock private lateinit var udfpsKeyguardViewModels: Provider<UdfpsKeyguardViewModels>
+ @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
@Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true }
@@ -164,7 +162,7 @@
alternateBouncerInteractor,
isDebuggable,
udfpsKeyguardAccessibilityDelegate,
- udfpsKeyguardViewModels,
+ keyguardTransitionInteractor,
mSelectedUserInteractor,
)
block()
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 675ca63..c8c400d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -86,6 +86,7 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels;
import com.android.systemui.log.SessionTracker;
import com.android.systemui.plugins.FalsingManager;
@@ -237,6 +238,8 @@
private ViewRootImpl mViewRootImpl;
@Mock
private FpsUnlockTracker mFpsUnlockTracker;
+ @Mock
+ private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@Before
public void setUp() {
@@ -329,7 +332,8 @@
mUdfpsKeyguardAccessibilityDelegate,
mUdfpsKeyguardViewModels,
mSelectedUserInteractor,
- mFpsUnlockTracker
+ mFpsUnlockTracker,
+ mKeyguardTransitionInteractor
);
verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
mOverlayController = mOverlayCaptor.getValue();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
index 2c4e136..2ea803c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
@@ -31,6 +31,7 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.shade.ShadeExpansionStateManager;
@@ -71,6 +72,7 @@
protected @Mock AlternateBouncerInteractor mAlternateBouncerInteractor;
protected @Mock UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate;
protected @Mock SelectedUserInteractor mSelectedUserInteractor;
+ protected @Mock KeyguardTransitionInteractor mKeyguardTransitionInteractor;
protected FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@@ -141,11 +143,11 @@
mDialogManager,
mUdfpsController,
mActivityLaunchAnimator,
- mFeatureFlags,
mPrimaryBouncerInteractor,
mAlternateBouncerInteractor,
mUdfpsKeyguardAccessibilityDelegate,
- mSelectedUserInteractor);
+ mSelectedUserInteractor,
+ mKeyguardTransitionInteractor);
return controller;
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerTest.java
index 21928cd..98d8b05 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerTest.java
@@ -66,17 +66,12 @@
public void testViewControllerQueriesSBStateOnAttached() {
mController.onViewAttached();
verify(mStatusBarStateController).getState();
- verify(mStatusBarStateController).getDozeAmount();
- final float dozeAmount = .88f;
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED);
- when(mStatusBarStateController.getDozeAmount()).thenReturn(dozeAmount);
captureStatusBarStateListeners();
mController.onViewAttached();
verify(mView, atLeast(1)).setPauseAuth(true);
- verify(mView).onDozeAmountChanged(dozeAmount, dozeAmount,
- UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN);
}
@Test
@@ -91,19 +86,6 @@
}
@Test
- public void testDozeEventsSentToView() {
- mController.onViewAttached();
- captureStatusBarStateListeners();
-
- final float linear = .55f;
- final float eased = .65f;
- mStatusBarStateListener.onDozeAmountChanged(linear, eased);
-
- verify(mView).onDozeAmountChanged(linear, eased,
- UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN);
- }
-
- @Test
public void testShouldPauseAuthUnpausedAlpha0() {
mController.onViewAttached();
captureStatusBarStateListeners();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
index 97dada2..a49150f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
@@ -31,7 +31,12 @@
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.StatusBarState
@@ -49,6 +54,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.mock
@@ -65,6 +71,7 @@
private val testScope = TestScope(testDispatcher)
private lateinit var keyguardBouncerRepository: KeyguardBouncerRepository
+ private lateinit var transitionRepository: FakeKeyguardTransitionRepository
@Mock private lateinit var bouncerLogger: TableLogBuffer
@@ -78,6 +85,7 @@
testScope.backgroundScope,
bouncerLogger,
)
+ transitionRepository = FakeKeyguardTransitionRepository()
super.setUp()
}
@@ -107,6 +115,12 @@
mock(SystemClock::class.java),
mKeyguardUpdateMonitor,
)
+ mKeyguardTransitionInteractor =
+ KeyguardTransitionInteractorFactory.create(
+ scope = testScope.backgroundScope,
+ repository = transitionRepository,
+ )
+ .keyguardTransitionInteractor
return createUdfpsKeyguardViewController(/* useModernBouncer */ true)
}
@@ -258,4 +272,145 @@
job.cancel()
}
+
+ @Test
+ fun aodToLockscreen_dozeAmountChanged() =
+ testScope.runTest {
+ // GIVEN view is attached
+ mController.onViewAttached()
+ Mockito.reset(mView)
+
+ val job = mController.listenForLockscreenAodTransitions(this)
+
+ // WHEN transitioning from lockscreen to aod
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ value = .3f,
+ transitionState = TransitionState.RUNNING
+ )
+ )
+ runCurrent()
+ // THEN doze amount is updated
+ verify(mView)
+ .onDozeAmountChanged(
+ eq(.3f),
+ eq(.3f),
+ eq(UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN)
+ )
+
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ value = 1f,
+ transitionState = TransitionState.FINISHED
+ )
+ )
+ runCurrent()
+ // THEN doze amount is updated
+ verify(mView)
+ .onDozeAmountChanged(
+ eq(1f),
+ eq(1f),
+ eq(UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN)
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun lockscreenToAod_dozeAmountChanged() =
+ testScope.runTest {
+ // GIVEN view is attached
+ mController.onViewAttached()
+ Mockito.reset(mView)
+
+ val job = mController.listenForLockscreenAodTransitions(this)
+
+ // WHEN transitioning from lockscreen to aod
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ value = .3f,
+ transitionState = TransitionState.RUNNING
+ )
+ )
+ runCurrent()
+ // THEN doze amount is updated
+ verify(mView)
+ .onDozeAmountChanged(
+ eq(.3f),
+ eq(.3f),
+ eq(UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN)
+ )
+
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ value = 1f,
+ transitionState = TransitionState.FINISHED
+ )
+ )
+ runCurrent()
+ // THEN doze amount is updated
+ verify(mView)
+ .onDozeAmountChanged(
+ eq(1f),
+ eq(1f),
+ eq(UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN)
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun goneToAod_dozeAmountChanged() =
+ testScope.runTest {
+ // GIVEN view is attached
+ mController.onViewAttached()
+ Mockito.reset(mView)
+
+ val job = mController.listenForGoneToAodTransition(this)
+
+ // WHEN transitioning from lockscreen to aod
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ value = .3f,
+ transitionState = TransitionState.RUNNING
+ )
+ )
+ runCurrent()
+ // THEN doze amount is updated
+ verify(mView)
+ .onDozeAmountChanged(
+ eq(.3f),
+ eq(.3f),
+ eq(UdfpsKeyguardViewLegacy.ANIMATION_UNLOCKED_SCREEN_OFF)
+ )
+
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ value = 1f,
+ transitionState = TransitionState.FINISHED
+ )
+ )
+ runCurrent()
+ // THEN doze amount is updated
+ verify(mView)
+ .onDozeAmountChanged(
+ eq(1f),
+ eq(1f),
+ eq(UdfpsKeyguardViewLegacy.ANIMATION_UNLOCKED_SCREEN_OFF)
+ )
+
+ job.cancel()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index c498edf..9b1e958 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -78,12 +78,28 @@
lockDeviceAndOpenPasswordBouncer()
assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
- assertThat(password).isEqualTo("")
+ assertThat(password).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Password)
}
@Test
+ fun onHidden_resetsPasswordInputAndMessage() =
+ testScope.runTest {
+ val message by collectLastValue(bouncerViewModel.message)
+ val password by collectLastValue(underTest.password)
+ lockDeviceAndOpenPasswordBouncer()
+
+ underTest.onPasswordInputChanged("password")
+ assertThat(message?.text).isNotEqualTo(ENTER_YOUR_PASSWORD)
+ assertThat(password).isNotEmpty()
+
+ underTest.onHidden()
+ assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
+ assertThat(password).isEmpty()
+ }
+
+ @Test
fun onPasswordInputChanged() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.desiredScene)
@@ -121,7 +137,7 @@
underTest.onPasswordInputChanged("wrong")
underTest.onAuthenticateKeyPressed()
- assertThat(password).isEqualTo("")
+ assertThat(password).isEmpty()
assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
}
@@ -134,14 +150,13 @@
AuthenticationMethodModel.Password
)
utils.deviceEntryRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
- underTest.onShown()
- // Enter nothing.
+ switchToScene(SceneKey.Bouncer)
+
+ // No input entered.
underTest.onAuthenticateKeyPressed()
- assertThat(password).isEqualTo("")
+ assertThat(password).isEmpty()
assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
}
@@ -182,32 +197,33 @@
assertThat(password).isEqualTo("password")
// The user doesn't confirm the password, but navigates back to the lockscreen instead.
- sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason")
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
+ switchToScene(SceneKey.Lockscreen)
// The user navigates to the bouncer again.
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-
- underTest.onShown()
+ switchToScene(SceneKey.Bouncer)
// Ensure the previously-entered password is not shown.
assertThat(password).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
+ private fun TestScope.switchToScene(toScene: SceneKey) {
+ val currentScene by collectLastValue(sceneInteractor.desiredScene)
+ val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer
+ val bouncerHidden = currentScene?.key == SceneKey.Bouncer && toScene != SceneKey.Bouncer
+ sceneInteractor.changeScene(SceneModel(toScene), "reason")
+ sceneInteractor.onSceneChanged(SceneModel(toScene), "reason")
+ if (bouncerShown) underTest.onShown()
+ if (bouncerHidden) underTest.onHidden()
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(SceneModel(toScene))
+ }
+
private fun TestScope.lockDeviceAndOpenPasswordBouncer() {
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Password)
utils.deviceEntryRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-
- assertThat(collectLastValue(sceneInteractor.desiredScene).invoke())
- .isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
- runCurrent()
+ switchToScene(SceneKey.Bouncer)
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 3f5ddba..125fe68 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -373,15 +373,23 @@
)
}
+ private fun TestScope.switchToScene(toScene: SceneKey) {
+ val currentScene by collectLastValue(sceneInteractor.desiredScene)
+ val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer
+ val bouncerHidden = currentScene?.key == SceneKey.Bouncer && toScene != SceneKey.Bouncer
+ sceneInteractor.changeScene(SceneModel(toScene), "reason")
+ sceneInteractor.onSceneChanged(SceneModel(toScene), "reason")
+ if (bouncerShown) underTest.onShown()
+ if (bouncerHidden) underTest.onHidden()
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(SceneModel(toScene))
+ }
+
private fun TestScope.lockDeviceAndOpenPatternBouncer() {
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pattern)
utils.deviceEntryRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
- assertThat(collectLastValue(sceneInteractor.desiredScene).invoke())
- .isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
- runCurrent()
+ switchToScene(SceneKey.Bouncer)
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 52844cf..c30e405 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -76,20 +76,12 @@
@Test
fun onShown() =
testScope.runTest {
- val currentScene by collectLastValue(sceneInteractor.desiredScene)
val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
- utils.deviceEntryRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-
- underTest.onShown()
+ lockDeviceAndOpenPinBouncer()
assertThat(message?.text).ignoringCase().isEqualTo(ENTER_YOUR_PIN)
assertThat(pin).isEmpty()
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pin)
}
@@ -142,29 +134,19 @@
@Test
fun onPinButtonClicked() =
testScope.runTest {
- val currentScene by collectLastValue(sceneInteractor.desiredScene)
val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
- utils.deviceEntryRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
- runCurrent()
+ lockDeviceAndOpenPinBouncer()
underTest.onPinButtonClicked(1)
assertThat(message?.text).isEmpty()
assertThat(pin).containsExactly(1)
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@Test
fun onBackspaceButtonClicked() =
testScope.runTest {
- val currentScene by collectLastValue(sceneInteractor.desiredScene)
val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
@@ -176,7 +158,6 @@
assertThat(message?.text).isEmpty()
assertThat(pin).isEmpty()
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@Test
@@ -224,9 +205,7 @@
collectLastValue(authenticationInteractor.authenticationChallengeResult)
lockDeviceAndOpenPinBouncer()
- FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
- underTest.onPinButtonClicked(digit)
- }
+ FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
underTest.onAuthenticateButtonClicked()
@@ -274,9 +253,7 @@
assertThat(authResult).isFalse()
// Enter the correct PIN:
- FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
- underTest.onPinButtonClicked(digit)
- }
+ FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
assertThat(message?.text).isEmpty()
underTest.onAuthenticateButtonClicked()
@@ -292,9 +269,7 @@
collectLastValue(authenticationInteractor.authenticationChallengeResult)
lockDeviceAndOpenPinBouncer()
- FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
- underTest.onPinButtonClicked(digit)
- }
+ FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
assertThat(authResult).isTrue()
}
@@ -323,31 +298,21 @@
@Test
fun onShown_againAfterSceneChange_resetsPin() =
testScope.runTest {
- val currentScene by collectLastValue(sceneInteractor.desiredScene)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
// The user types a PIN.
- FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
- underTest.onPinButtonClicked(digit)
- }
+ FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
assertThat(pin).isNotEmpty()
// The user doesn't confirm the PIN, but navigates back to the lockscreen instead.
- sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason")
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
+ switchToScene(SceneKey.Lockscreen)
// The user navigates to the bouncer again.
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-
- underTest.onShown()
+ switchToScene(SceneKey.Bouncer)
// Ensure the previously-entered PIN is not shown.
assertThat(pin).isEmpty()
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@Test
@@ -414,16 +379,23 @@
assertThat(isAnimationEnabled).isTrue()
}
+ private fun TestScope.switchToScene(toScene: SceneKey) {
+ val currentScene by collectLastValue(sceneInteractor.desiredScene)
+ val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer
+ val bouncerHidden = currentScene?.key == SceneKey.Bouncer && toScene != SceneKey.Bouncer
+ sceneInteractor.changeScene(SceneModel(toScene), "reason")
+ sceneInteractor.onSceneChanged(SceneModel(toScene), "reason")
+ if (bouncerShown) underTest.onShown()
+ if (bouncerHidden) underTest.onHidden()
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(SceneModel(toScene))
+ }
+
private fun TestScope.lockDeviceAndOpenPinBouncer() {
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.deviceEntryRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-
- assertThat(collectLastValue(sceneInteractor.desiredScene).invoke())
- .isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
- runCurrent()
+ switchToScene(SceneKey.Bouncer)
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index 3b07913..5245b22 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -27,7 +27,6 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -41,7 +40,6 @@
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.testing.AndroidTestingRunner;
import android.util.SparseArray;
import android.view.View;
@@ -82,7 +80,6 @@
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -107,10 +104,6 @@
ComponentName.unflattenFromString("TEST_PKG/.TEST_CLS");
private static final String CUSTOM_TILE_SPEC = CustomTile.toSpec(CUSTOM_TILE);
private static final String SETTING = QSHost.TILES_SETTING;
-
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
-
@Mock
private PluginManager mPluginManager;
@Mock
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
index 6cc52d7..fbd63c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
@@ -403,6 +403,31 @@
verify(falseContext).bindServiceAsUser(any(), any(), eq(flags), any());
}
+ @Test
+ public void testNullBindingCallsUnbind() {
+ Context mockContext = mock(Context.class);
+ // Binding has to succeed
+ when(mockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
+ TileLifecycleManager manager = new TileLifecycleManager(mHandler, mockContext,
+ mock(IQSService.class),
+ mMockPackageManagerAdapter,
+ mMockBroadcastDispatcher,
+ mTileServiceIntent,
+ mUser,
+ mActivityManager,
+ mExecutor);
+
+ manager.executeSetBindService(true);
+ mExecutor.runAllReady();
+
+ ArgumentCaptor<ServiceConnection> captor = ArgumentCaptor.forClass(ServiceConnection.class);
+ verify(mockContext).bindServiceAsUser(any(), captor.capture(), anyInt(), any());
+
+ captor.getValue().onNullBinding(mTileServiceComponentName);
+ mExecutor.runAllReady();
+ verify(mockContext).unbindService(captor.getValue());
+ }
+
private void mockChangeEnabled(long changeId, boolean enabled) {
doReturn(enabled).when(() -> CompatChanges.isChangeEnabled(eq(changeId), anyString(),
any(UserHandle.class)));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index 355ca81..8c896a6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -21,7 +21,6 @@
import android.content.Intent
import android.content.pm.UserInfo
import android.os.UserHandle
-import android.platform.test.flag.junit.SetFlagsRule
import android.service.quicksettings.Tile
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -62,7 +61,6 @@
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyString
@@ -77,8 +75,6 @@
@OptIn(ExperimentalCoroutinesApi::class)
class CurrentTilesInteractorImplTest : SysuiTestCase() {
- @Rule @JvmField val setFlagsRule = SetFlagsRule()
-
private val tileSpecRepository: TileSpecRepository = FakeTileSpecRepository()
private val userRepository = FakeUserRepository()
private val installedTilesPackageRepository = FakeInstalledTilesComponentRepository()
@@ -109,7 +105,7 @@
fun setup() {
MockitoAnnotations.initMocks(this)
- setFlagsRule.enableFlags(FLAG_QS_NEW_PIPELINE)
+ mSetFlagsRule.enableFlags(FLAG_QS_NEW_PIPELINE)
// TODO(b/299909337): Add test checking the new factory is used when the flag is on
featureFlags.set(Flags.QS_PIPELINE_NEW_TILES, true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt
index 62ca965..2e63708 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt
@@ -1,13 +1,11 @@
package com.android.systemui.qs.pipeline.shared
-import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -15,22 +13,20 @@
@RunWith(AndroidJUnit4::class)
class QSPipelineFlagsRepositoryTest : SysuiTestCase() {
- @Rule @JvmField val setFlagsRule = SetFlagsRule()
-
private val fakeFeatureFlagsClassic = FakeFeatureFlagsClassic()
private val underTest = QSPipelineFlagsRepository(fakeFeatureFlagsClassic)
@Test
fun pipelineFlagDisabled() {
- setFlagsRule.disableFlags(Flags.FLAG_QS_NEW_PIPELINE)
+ mSetFlagsRule.disableFlags(Flags.FLAG_QS_NEW_PIPELINE)
assertThat(underTest.pipelineEnabled).isFalse()
}
@Test
fun pipelineFlagEnabled() {
- setFlagsRule.enableFlags(Flags.FLAG_QS_NEW_PIPELINE)
+ mSetFlagsRule.enableFlags(Flags.FLAG_QS_NEW_PIPELINE)
assertThat(underTest.pipelineEnabled).isTrue()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index 1c62161..3e331a6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -76,6 +76,7 @@
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.FakeEventLog;
import com.android.systemui.util.settings.FakeGlobalSettings;
import com.android.systemui.util.time.FakeSystemClock;
@@ -126,6 +127,7 @@
DeviceProvisionedController mDeviceProvisionedController;
FakeSystemClock mSystemClock;
FakeGlobalSettings mGlobalSettings;
+ FakeEventLog mEventLog;
private NotificationInterruptStateProviderImpl mNotifInterruptionStateProvider;
@@ -138,6 +140,7 @@
mSystemClock = new FakeSystemClock();
mGlobalSettings = new FakeGlobalSettings();
mGlobalSettings.putInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_ON);
+ mEventLog = new FakeEventLog();
mNotifInterruptionStateProvider =
new NotificationInterruptStateProviderImpl(
@@ -155,7 +158,8 @@
mUserTracker,
mDeviceProvisionedController,
mSystemClock,
- mGlobalSettings);
+ mGlobalSettings,
+ mEventLog);
mNotifInterruptionStateProvider.mUseHeadsUp = true;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt
index e1581ea..acc5cea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt
@@ -53,6 +53,7 @@
deviceProvisionedController,
systemClock,
globalSettings,
+ eventLog
)
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
index 1064475..9e7df5f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
@@ -33,6 +33,7 @@
ambientDisplayConfiguration,
batteryController,
deviceProvisionedController,
+ eventLog,
globalSettings,
headsUpManager,
keyguardNotificationVisibilityProvider,
@@ -42,6 +43,7 @@
powerManager,
statusBarStateController,
systemClock,
+ uiEventLogger,
userTracker,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
index 5e81156..5dcb6c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
@@ -47,6 +47,7 @@
import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED
import android.provider.Settings.Global.HEADS_UP_OFF
import android.provider.Settings.Global.HEADS_UP_ON
+import com.android.internal.logging.UiEventLogger.UiEventEnum
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.LogBuffer
@@ -63,8 +64,13 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.MAX_HUN_WHEN_AGE_MS
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.HUN_SUPPRESSED_OLD_WHEN
import com.android.systemui.statusbar.policy.FakeDeviceProvisionedController
import com.android.systemui.statusbar.policy.HeadsUpManager
+import com.android.systemui.util.FakeEventLog
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.settings.FakeGlobalSettings
@@ -100,6 +106,7 @@
protected val ambientDisplayConfiguration = FakeAmbientDisplayConfiguration(context)
protected val batteryController = FakeBatteryController(leakCheck)
protected val deviceProvisionedController = FakeDeviceProvisionedController()
+ protected val eventLog = FakeEventLog()
protected val flags: NotifPipelineFlags = mock()
protected val globalSettings =
FakeGlobalSettings().also { it.putInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_ON) }
@@ -147,18 +154,21 @@
fun testShouldPeek() {
ensurePeekState()
assertShouldHeadsUp(buildPeekEntry())
+ assertNoEventsLogged()
}
@Test
fun testShouldNotPeek_settingDisabled() {
ensurePeekState { hunSettingEnabled = false }
assertShouldNotHeadsUp(buildPeekEntry())
+ assertNoEventsLogged()
}
@Test
fun testShouldNotPeek_packageSnoozed_withoutFsi() {
ensurePeekState { hunSnoozed = true }
assertShouldNotHeadsUp(buildPeekEntry())
+ assertNoEventsLogged()
}
@Test
@@ -167,6 +177,13 @@
forEachPeekableFsiState {
ensurePeekState { hunSnoozed = true }
assertShouldHeadsUp(entry)
+
+ // The old code logs a UiEvent when a HUN snooze is bypassed because the notification
+ // has an FSI, but that doesn't fit into the new code's suppressor-based logic, so we're
+ // not reimplementing it.
+ if (provider !is NotificationInterruptStateProviderWrapper) {
+ assertNoEventsLogged()
+ }
}
}
@@ -174,42 +191,49 @@
fun testShouldNotPeek_alreadyBubbled() {
ensurePeekState { statusBarState = SHADE }
assertShouldNotHeadsUp(buildPeekEntry { isBubble = true })
+ assertNoEventsLogged()
}
@Test
fun testShouldPeek_isBubble_shadeLocked() {
ensurePeekState { statusBarState = SHADE_LOCKED }
assertShouldHeadsUp(buildPeekEntry { isBubble = true })
+ assertNoEventsLogged()
}
@Test
fun testShouldPeek_isBubble_keyguard() {
ensurePeekState { statusBarState = KEYGUARD }
assertShouldHeadsUp(buildPeekEntry { isBubble = true })
+ assertNoEventsLogged()
}
@Test
fun testShouldNotPeek_dnd() {
ensurePeekState()
assertShouldNotHeadsUp(buildPeekEntry { suppressedVisualEffects = SUPPRESSED_EFFECT_PEEK })
+ assertNoEventsLogged()
}
@Test
fun testShouldNotPeek_notImportant() {
ensurePeekState()
assertShouldNotHeadsUp(buildPeekEntry { importance = IMPORTANCE_DEFAULT })
+ assertNoEventsLogged()
}
@Test
fun testShouldNotPeek_screenOff() {
ensurePeekState { isScreenOn = false }
assertShouldNotHeadsUp(buildPeekEntry())
+ assertNoEventsLogged()
}
@Test
fun testShouldNotPeek_dreaming() {
ensurePeekState { isDreaming = true }
assertShouldNotHeadsUp(buildPeekEntry())
+ assertNoEventsLogged()
}
@Test
@@ -219,33 +243,56 @@
}
@Test
+ fun testLogsHunOldWhen() {
+ assertNoEventsLogged()
+
+ ensurePeekState()
+ val entry = buildPeekEntry { whenMs = whenAgo(MAX_HUN_WHEN_AGE_MS) }
+
+ // The old code logs the "old when" UiEvent unconditionally, so don't expect that it hasn't.
+ if (provider !is NotificationInterruptStateProviderWrapper) {
+ provider.makeUnloggedHeadsUpDecision(entry)
+ assertNoEventsLogged()
+ }
+
+ provider.makeAndLogHeadsUpDecision(entry)
+ assertUiEventLogged(HUN_SUPPRESSED_OLD_WHEN, entry.sbn.uid, entry.sbn.packageName)
+ assertNoSystemEventLogged()
+ }
+
+ @Test
fun testShouldPeek_oldWhen_now() {
ensurePeekState()
assertShouldHeadsUp(buildPeekEntry { whenMs = whenAgo(0) })
+ assertNoEventsLogged()
}
@Test
fun testShouldPeek_oldWhen_notOldEnough() {
ensurePeekState()
assertShouldHeadsUp(buildPeekEntry { whenMs = whenAgo(MAX_HUN_WHEN_AGE_MS - 1) })
+ assertNoEventsLogged()
}
@Test
fun testShouldPeek_oldWhen_zeroWhen() {
ensurePeekState()
assertShouldHeadsUp(buildPeekEntry { whenMs = 0L })
+ assertNoEventsLogged()
}
@Test
fun testShouldPeek_oldWhen_negativeWhen() {
ensurePeekState()
assertShouldHeadsUp(buildPeekEntry { whenMs = -1L })
+ assertNoEventsLogged()
}
@Test
fun testShouldPeek_oldWhen_fullScreenIntent() {
ensurePeekState()
assertShouldHeadsUp(buildFsiEntry { whenMs = whenAgo(MAX_HUN_WHEN_AGE_MS) })
+ assertNoEventsLogged()
}
@Test
@@ -257,6 +304,7 @@
isForegroundService = true
}
)
+ assertNoEventsLogged()
}
@Test
@@ -268,18 +316,21 @@
isUserInitiatedJob = true
}
)
+ assertNoEventsLogged()
}
@Test
fun testShouldNotPeek_hiddenOnKeyguard() {
ensurePeekState({ keyguardShouldHideNotification = true })
assertShouldNotHeadsUp(buildPeekEntry())
+ assertNoEventsLogged()
}
@Test
fun testShouldPeek_defaultLegacySuppressor() {
ensurePeekState()
withLegacySuppressor(neverSuppresses) { assertShouldHeadsUp(buildPeekEntry()) }
+ assertNoEventsLogged()
}
@Test
@@ -288,6 +339,7 @@
withLegacySuppressor(alwaysSuppressesInterruptions) {
assertShouldNotHeadsUp(buildPeekEntry())
}
+ assertNoEventsLogged()
}
@Test
@@ -296,6 +348,7 @@
withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
assertShouldNotHeadsUp(buildPeekEntry())
}
+ assertNoEventsLogged()
}
@Test
@@ -304,24 +357,28 @@
withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) {
assertShouldNotHeadsUp(buildPeekEntry())
}
+ assertNoEventsLogged()
}
@Test
fun testShouldPulse() {
ensurePulseState()
assertShouldHeadsUp(buildPulseEntry())
+ assertNoEventsLogged()
}
@Test
fun testShouldNotPulse_disabled() {
ensurePulseState { pulseOnNotificationsEnabled = false }
assertShouldNotHeadsUp(buildPulseEntry())
+ assertNoEventsLogged()
}
@Test
fun testShouldNotPulse_batterySaver() {
ensurePulseState { isAodPowerSave = true }
assertShouldNotHeadsUp(buildPulseEntry())
+ assertNoEventsLogged()
}
@Test
@@ -330,30 +387,35 @@
assertShouldNotHeadsUp(
buildPulseEntry { suppressedVisualEffects = SUPPRESSED_EFFECT_AMBIENT }
)
+ assertNoEventsLogged()
}
@Test
fun testShouldNotPulse_visibilityOverridePrivate() {
ensurePulseState()
assertShouldNotHeadsUp(buildPulseEntry { visibilityOverride = VISIBILITY_PRIVATE })
+ assertNoEventsLogged()
}
@Test
fun testShouldNotPulse_importanceLow() {
ensurePulseState()
assertShouldNotHeadsUp(buildPulseEntry { importance = IMPORTANCE_LOW })
+ assertNoEventsLogged()
}
@Test
fun testShouldNotPulse_hiddenOnKeyguard() {
ensurePulseState({ keyguardShouldHideNotification = true })
assertShouldNotHeadsUp(buildPulseEntry())
+ assertNoEventsLogged()
}
@Test
fun testShouldPulse_defaultLegacySuppressor() {
ensurePulseState()
withLegacySuppressor(neverSuppresses) { assertShouldHeadsUp(buildPulseEntry()) }
+ assertNoEventsLogged()
}
@Test
@@ -362,6 +424,7 @@
withLegacySuppressor(alwaysSuppressesInterruptions) {
assertShouldNotHeadsUp(buildPulseEntry())
}
+ assertNoEventsLogged()
}
@Test
@@ -370,6 +433,7 @@
withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
assertShouldHeadsUp(buildPulseEntry())
}
+ assertNoEventsLogged()
}
@Test
@@ -378,6 +442,7 @@
withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) {
assertShouldHeadsUp(buildPulseEntry())
}
+ assertNoEventsLogged()
}
private fun withPeekAndPulseEntry(
@@ -399,6 +464,7 @@
groupAlertBehavior = GROUP_ALERT_SUMMARY
}) {
assertShouldNotHeadsUp(it)
+ assertNoEventsLogged()
}
}
@@ -410,6 +476,7 @@
groupAlertBehavior = GROUP_ALERT_CHILDREN
}) {
assertShouldHeadsUp(it)
+ assertNoEventsLogged()
}
}
@@ -421,24 +488,30 @@
groupAlertBehavior = GROUP_ALERT_SUMMARY
}) {
assertShouldHeadsUp(it)
+ assertNoEventsLogged()
}
}
@Test
fun testShouldNotHeadsUp_justLaunchedFsi() {
- withPeekAndPulseEntry({ hasJustLaunchedFsi = true }) { assertShouldNotHeadsUp(it) }
+ withPeekAndPulseEntry({ hasJustLaunchedFsi = true }) {
+ assertShouldNotHeadsUp(it)
+ assertNoEventsLogged()
+ }
}
@Test
fun testShouldBubble_withIntentAndIcon() {
ensureBubbleState()
assertShouldBubble(buildBubbleEntry { bubbleIsShortcut = false })
+ assertNoEventsLogged()
}
@Test
fun testShouldBubble_withShortcut() {
ensureBubbleState()
assertShouldBubble(buildBubbleEntry { bubbleIsShortcut = true })
+ assertNoEventsLogged()
}
@Test
@@ -451,6 +524,7 @@
groupAlertBehavior = GROUP_ALERT_SUMMARY
}
)
+ assertNoEventsLogged()
}
@Test
@@ -462,24 +536,28 @@
hasBubbleMetadata = false
}
)
+ assertNoEventsLogged()
}
@Test
fun testShouldNotBubble_missingBubbleMetadata() {
ensureBubbleState()
assertShouldNotBubble(buildBubbleEntry { hasBubbleMetadata = false })
+ assertNoEventsLogged()
}
@Test
fun testShouldNotBubble_notAllowedToBubble() {
ensureBubbleState()
assertShouldNotBubble(buildBubbleEntry { canBubble = false })
+ assertNoEventsLogged()
}
@Test
fun testShouldBubble_defaultLegacySuppressor() {
ensureBubbleState()
withLegacySuppressor(neverSuppresses) { assertShouldBubble(buildBubbleEntry()) }
+ assertNoEventsLogged()
}
@Test
@@ -488,6 +566,7 @@
withLegacySuppressor(alwaysSuppressesInterruptions) {
assertShouldNotBubble(buildBubbleEntry())
}
+ assertNoEventsLogged()
}
@Test
@@ -496,6 +575,7 @@
withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
assertShouldNotBubble(buildBubbleEntry())
}
+ assertNoEventsLogged()
}
@Test
@@ -504,17 +584,22 @@
withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) {
assertShouldBubble(buildBubbleEntry())
}
+ assertNoEventsLogged()
}
@Test
fun testShouldNotBubble_hiddenOnKeyguard() {
ensureBubbleState({ keyguardShouldHideNotification = true })
assertShouldNotBubble(buildBubbleEntry())
+ assertNoEventsLogged()
}
@Test
fun testShouldNotFsi_noFullScreenIntent() {
- forEachFsiState { assertShouldNotFsi(buildFsiEntry { hasFsi = false }) }
+ forEachFsiState {
+ assertShouldNotFsi(buildFsiEntry { hasFsi = false })
+ assertNoEventsLogged()
+ }
}
@Test
@@ -526,6 +611,7 @@
isStickyAndNotDemoted = true
}
)
+ assertNoEventsLogged()
}
}
@@ -536,12 +622,16 @@
buildFsiEntry { suppressedVisualEffects = SUPPRESSED_EFFECT_FULL_SCREEN_INTENT },
expectWouldInterruptWithoutDnd = true
)
+ assertNoEventsLogged()
}
}
@Test
fun testShouldNotFsi_notImportantEnough() {
- forEachFsiState { assertShouldNotFsi(buildFsiEntry { importance = IMPORTANCE_DEFAULT }) }
+ forEachFsiState {
+ assertShouldNotFsi(buildFsiEntry { importance = IMPORTANCE_DEFAULT })
+ assertNoEventsLogged()
+ }
}
@Test
@@ -554,6 +644,7 @@
},
expectWouldInterruptWithoutDnd = false
)
+ assertNoEventsLogged()
}
}
@@ -571,6 +662,27 @@
}
@Test
+ fun testLogsFsiSuppressiveGroupAlertBehavior() {
+ ensureNotInteractiveFsiState()
+ val entry = buildFsiEntry {
+ isGrouped = true
+ isGroupSummary = true
+ groupAlertBehavior = GROUP_ALERT_CHILDREN
+ }
+
+ val decision = provider.makeUnloggedFullScreenIntentDecision(entry)
+ assertNoEventsLogged()
+
+ provider.logFullScreenIntentDecision(decision)
+ assertUiEventLogged(
+ FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR,
+ entry.sbn.uid,
+ entry.sbn.packageName
+ )
+ assertSystemEventLogged("231322873", entry.sbn.uid, "groupAlertBehavior")
+ }
+
+ @Test
fun testShouldFsi_suppressiveGroupAlertBehavior_notGrouped() {
forEachFsiState {
assertShouldFsi(
@@ -580,6 +692,7 @@
groupAlertBehavior = GROUP_ALERT_CHILDREN
}
)
+ assertNoEventsLogged()
}
}
@@ -609,26 +722,52 @@
}
@Test
+ fun testLogsFsiSuppressiveBubbleMetadata() {
+ ensureNotInteractiveFsiState()
+ val entry = buildFsiEntry {
+ hasBubbleMetadata = true
+ bubbleSuppressesNotification = true
+ }
+
+ val decision = provider.makeUnloggedFullScreenIntentDecision(entry)
+ assertNoEventsLogged()
+
+ provider.logFullScreenIntentDecision(decision)
+ assertUiEventLogged(
+ FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA,
+ entry.sbn.uid,
+ entry.sbn.packageName
+ )
+ assertSystemEventLogged("274759612", entry.sbn.uid, "bubbleMetadata")
+ }
+
+ @Test
fun testShouldNotFsi_packageSuspended() {
- forEachFsiState { assertShouldNotFsi(buildFsiEntry { packageSuspended = true }) }
+ forEachFsiState {
+ assertShouldNotFsi(buildFsiEntry { packageSuspended = true })
+ assertNoEventsLogged()
+ }
}
@Test
fun testShouldFsi_notInteractive() {
ensureNotInteractiveFsiState()
assertShouldFsi(buildFsiEntry())
+ assertNoEventsLogged()
}
@Test
fun testShouldFsi_dreaming() {
ensureDreamingFsiState()
assertShouldFsi(buildFsiEntry())
+ assertNoEventsLogged()
}
@Test
fun testShouldFsi_keyguard() {
ensureKeyguardFsiState()
assertShouldFsi(buildFsiEntry())
+ assertNoEventsLogged()
}
@Test
@@ -636,6 +775,7 @@
forEachPeekableFsiState {
ensurePeekState()
assertShouldNotFsi(buildFsiEntry())
+ assertNoEventsLogged()
}
}
@@ -644,6 +784,7 @@
forEachPeekableFsiState {
ensurePeekState { hunSnoozed = true }
assertShouldNotFsi(buildFsiEntry())
+ assertNoEventsLogged()
}
}
@@ -651,18 +792,21 @@
fun testShouldFsi_lockedShade() {
ensureLockedShadeFsiState()
assertShouldFsi(buildFsiEntry())
+ assertNoEventsLogged()
}
@Test
fun testShouldFsi_keyguardOccluded() {
ensureKeyguardOccludedFsiState()
assertShouldFsi(buildFsiEntry())
+ assertNoEventsLogged()
}
@Test
fun testShouldFsi_deviceNotProvisioned() {
ensureDeviceNotProvisionedFsiState()
assertShouldFsi(buildFsiEntry())
+ assertNoEventsLogged()
}
@Test
@@ -672,9 +816,23 @@
}
@Test
+ fun testLogsFsiNoHunOrKeyguard() {
+ ensureNoHunOrKeyguardFsiState()
+ val entry = buildFsiEntry()
+
+ val decision = provider.makeUnloggedFullScreenIntentDecision(entry)
+ assertNoEventsLogged()
+
+ provider.logFullScreenIntentDecision(decision)
+ assertUiEventLogged(FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD, entry.sbn.uid, entry.sbn.packageName)
+ assertSystemEventLogged("231322873", entry.sbn.uid, "no hun or keyguard")
+ }
+
+ @Test
fun testShouldFsi_defaultLegacySuppressor() {
forEachFsiState {
withLegacySuppressor(neverSuppresses) { assertShouldFsi(buildFsiEntry()) }
+ assertNoEventsLogged()
}
}
@@ -682,6 +840,7 @@
fun testShouldFsi_suppressInterruptions() {
forEachFsiState {
withLegacySuppressor(alwaysSuppressesInterruptions) { assertShouldFsi(buildFsiEntry()) }
+ assertNoEventsLogged()
}
}
@@ -691,6 +850,7 @@
withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
assertShouldFsi(buildFsiEntry())
}
+ assertNoEventsLogged()
}
}
@@ -698,6 +858,7 @@
fun testShouldFsi_suppressAwakeHeadsUp() {
forEachFsiState {
withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) { assertShouldFsi(buildFsiEntry()) }
+ assertNoEventsLogged()
}
}
@@ -1080,6 +1241,45 @@
run(block)
}
+ private fun assertNoEventsLogged() {
+ assertNoUiEventLogged()
+ assertNoSystemEventLogged()
+ }
+
+ private fun assertNoUiEventLogged() {
+ assertEquals(0, uiEventLogger.numLogs())
+ }
+
+ private fun assertUiEventLogged(uiEventId: UiEventEnum, uid: Int, packageName: String) {
+ assertEquals(1, uiEventLogger.numLogs())
+
+ val event = uiEventLogger.get(0)
+ assertEquals(uiEventId.id, event.eventId)
+ assertEquals(uid, event.uid)
+ assertEquals(packageName, event.packageName)
+ }
+
+ private fun assertNoSystemEventLogged() {
+ assertEquals(0, eventLog.events.size)
+ }
+
+ private fun assertSystemEventLogged(number: String, uid: Int, description: String) {
+ assertEquals(1, eventLog.events.size)
+
+ val event = eventLog.events[0]
+ assertEquals(0x534e4554, event.tag)
+
+ val value = event.value
+ assertTrue(value is Array<*>)
+
+ if (value is Array<*>) {
+ assertEquals(3, value.size)
+ assertEquals(number, value[0])
+ assertEquals(uid, value[1])
+ assertEquals(description, value[2])
+ }
+ }
+
private fun whenAgo(whenAgeMs: Long) = systemClock.currentTimeMillis() - whenAgeMs
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
index 1d8a346..84cd518 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.phone;
+import static com.android.systemui.Flags.FLAG_QS_NEW_PIPELINE;
import static com.android.systemui.qs.dagger.QSFlagsModule.RBC_AVAILABLE;
import static com.android.systemui.statusbar.phone.AutoTileManager.DEVICE_CONTROLS;
@@ -135,6 +136,8 @@
MockitoAnnotations.initMocks(this);
mSecureSettings = new FakeSettings();
+ mSetFlagsRule.disableFlags(FLAG_QS_NEW_PIPELINE);
+
mContext.getOrCreateTestableResources().addOverride(
R.array.config_quickSettingsAutoAdd,
new String[] {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 027c11c..251718d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -178,6 +178,8 @@
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.statusbar.window.StatusBarWindowController;
import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.util.EventLog;
+import com.android.systemui.util.FakeEventLog;
import com.android.systemui.util.WallpaperController;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.concurrency.MessageRouterImpl;
@@ -324,6 +326,7 @@
private ShadeController mShadeController;
private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
private final FakeGlobalSettings mFakeGlobalSettings = new FakeGlobalSettings();
+ private final FakeEventLog mFakeEventLog = new FakeEventLog();
private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
private final FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock);
private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@@ -374,7 +377,8 @@
mUserTracker,
mDeviceProvisionedController,
mFakeSystemClock,
- mFakeGlobalSettings);
+ mFakeGlobalSettings,
+ mFakeEventLog);
mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
mContext.addMockSystemService(FingerprintManager.class, mock(FingerprintManager.class));
@@ -1187,7 +1191,8 @@
UserTracker userTracker,
DeviceProvisionedController deviceProvisionedController,
SystemClock systemClock,
- GlobalSettings globalSettings) {
+ GlobalSettings globalSettings,
+ EventLog eventLog) {
super(
powerManager,
ambientDisplayConfiguration,
@@ -1203,7 +1208,8 @@
userTracker,
deviceProvisionedController,
systemClock,
- globalSettings
+ globalSettings,
+ eventLog
);
mUseHeadsUp = true;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 7e83034..aa5f987 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -162,6 +162,7 @@
import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
import com.android.systemui.user.domain.interactor.UserSwitcherInteractor;
+import com.android.systemui.util.FakeEventLog;
import com.android.systemui.util.settings.FakeGlobalSettings;
import com.android.systemui.util.time.SystemClock;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -543,7 +544,8 @@
mock(UserTracker.class),
mock(DeviceProvisionedController.class),
mock(SystemClock.class),
- fakeGlobalSettings
+ fakeGlobalSettings,
+ new FakeEventLog()
);
mShellTaskOrganizer = new ShellTaskOrganizer(mock(ShellInit.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
index 975555c..c9964c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
@@ -31,6 +31,7 @@
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.EventLog;
import com.android.systemui.util.settings.GlobalSettings;
import com.android.systemui.util.time.SystemClock;
@@ -52,7 +53,8 @@
UserTracker userTracker,
DeviceProvisionedController deviceProvisionedController,
SystemClock systemClock,
- GlobalSettings globalSettings) {
+ GlobalSettings globalSettings,
+ EventLog eventLog) {
super(
powerManager,
ambientDisplayConfiguration,
@@ -68,7 +70,8 @@
userTracker,
deviceProvisionedController,
systemClock,
- globalSettings);
+ globalSettings,
+ eventLog);
mUseHeadsUp = true;
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeEventLog.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeEventLog.kt
new file mode 100644
index 0000000..ea2eeabf
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeEventLog.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.systemui.util
+
+/** A fake [com.android.systemui.util.EventLog] for tests. */
+class FakeEventLog : EventLog {
+ data class Event(val tag: Int, val value: Any)
+
+ private val _events: MutableList<Event> = mutableListOf()
+ val events: List<Event>
+ get() = _events
+
+ fun clear() {
+ _events.clear()
+ }
+
+ override fun writeEvent(tag: Int, value: Int): Int {
+ _events.add(Event(tag, value))
+ return 1
+ }
+
+ override fun writeEvent(tag: Int, value: Long): Int {
+ _events.add(Event(tag, value))
+ return 1
+ }
+
+ override fun writeEvent(tag: Int, value: Float): Int {
+ _events.add(Event(tag, value))
+ return 1
+ }
+
+ override fun writeEvent(tag: Int, value: String): Int {
+ _events.add(Event(tag, value))
+ return 1
+ }
+
+ override fun writeEvent(tag: Int, vararg values: Any): Int {
+ _events.add(Event(tag, values))
+ return 1
+ }
+}
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index fc4ed1d..b9e34ee 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -33,3 +33,10 @@
],
visibility: ["//visibility:public"],
}
+
+java_host_for_device {
+ name: "core-xml-for-device",
+ libs: [
+ "core-xml-for-host",
+ ],
+}
diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt
index 692d598..3e54c7a 100644
--- a/ravenwood/framework-minus-apex-ravenwood-policies.txt
+++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt
@@ -103,7 +103,21 @@
# Containers
class android.os.BaseBundle stubclass
class android.os.Bundle stubclass
+class android.os.PersistableBundle stubclass
# Misc
class android.os.PatternMatcher stubclass
class android.os.ParcelUuid stubclass
+
+# XML
+class com.android.internal.util.XmlPullParserWrapper stubclass
+class com.android.internal.util.XmlSerializerWrapper stubclass
+class com.android.internal.util.XmlUtils stubclass
+
+class com.android.modules.utils.BinaryXmlPullParser stubclass
+class com.android.modules.utils.BinaryXmlSerializer stubclass
+class com.android.modules.utils.FastDataInput stubclass
+class com.android.modules.utils.FastDataOutput stubclass
+class com.android.modules.utils.ModifiedUtf8 stubclass
+class com.android.modules.utils.TypedXmlPullParser stubclass
+class com.android.modules.utils.TypedXmlSerializer stubclass
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index 776a19a..1ac6bf0 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -2,8 +2,11 @@
com.android.internal.util.ArrayUtils
+android.util.Xml
+
android.os.Binder
android.os.Binder$IdentitySupplier
android.os.IBinder
android.os.Process
android.os.SystemClock
+android.os.UserHandle
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionWrapper.java
similarity index 98%
rename from services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java
rename to services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionWrapper.java
index 6c6394f..f0c44d6 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionWrapper.java
@@ -35,15 +35,15 @@
/**
* A wrapper of {@link IWindowMagnificationConnection}.
*/
-class WindowMagnificationConnectionWrapper {
+class MagnificationConnectionWrapper {
private static final boolean DBG = false;
- private static final String TAG = "WindowMagnificationConnectionWrapper";
+ private static final String TAG = "MagnificationConnectionWrapper";
private final @NonNull IWindowMagnificationConnection mConnection;
private final @NonNull AccessibilityTraceManager mTrace;
- WindowMagnificationConnectionWrapper(@NonNull IWindowMagnificationConnection connection,
+ MagnificationConnectionWrapper(@NonNull IWindowMagnificationConnection connection,
@NonNull AccessibilityTraceManager trace) {
mConnection = connection;
mTrace = trace;
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
index 816f22f..3ea805b 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
@@ -60,7 +60,7 @@
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
/**
- * A class to manipulate window magnification through {@link WindowMagnificationConnectionWrapper}
+ * A class to manipulate window magnification through {@link MagnificationConnectionWrapper}
* create by {@link #setConnection(IWindowMagnificationConnection)}. To set the connection with
* SysUI, call {@code StatusBarManagerInternal#requestWindowMagnificationConnection(boolean)}.
* The applied magnification scale is constrained by
@@ -133,7 +133,7 @@
@VisibleForTesting
@GuardedBy("mLock")
@Nullable
- WindowMagnificationConnectionWrapper mConnectionWrapper;
+ MagnificationConnectionWrapper mConnectionWrapper;
@GuardedBy("mLock")
private ConnectionCallback mConnectionCallback;
@GuardedBy("mLock")
@@ -245,7 +245,7 @@
}
}
if (connection != null) {
- mConnectionWrapper = new WindowMagnificationConnectionWrapper(connection, mTrace);
+ mConnectionWrapper = new MagnificationConnectionWrapper(connection, mTrace);
}
if (mConnectionWrapper != null) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index f92af67..b2a7948 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -9367,6 +9367,8 @@
*/
void appendDropBoxProcessHeaders(ProcessRecord process, String processName,
final VolatileDropboxEntryStates volatileStates, final StringBuilder sb) {
+ sb.append("SystemUptimeMs: ").append(SystemClock.uptimeMillis()).append("\n");
+
// Watchdog thread ends up invoking this function (with
// a null ProcessRecord) to add the stack file to dropbox.
// Do not acquire a lock on this (am) in such cases, as it
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index f9fc4d4..36356bd 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -111,6 +111,7 @@
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.ParseUtils;
import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.NetworkCapabilitiesUtils;
import com.android.server.LocalServices;
import com.android.server.Watchdog;
@@ -475,7 +476,12 @@
ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE));
final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
try {
- nms.registerObserver(mActivityChangeObserver);
+ if (!SdkLevel.isAtLeastV()) {
+ // On V+ devices, ConnectivityService calls BatteryStats API to update
+ // RadioPowerState change. So BatteryStatsService registers the callback only on
+ // pre V devices.
+ nms.registerObserver(mActivityChangeObserver);
+ }
cm.registerDefaultNetworkCallback(mNetworkCallback);
} catch (RemoteException e) {
Slog.e(TAG, "Could not register INetworkManagement event observer " + e);
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index a0beedb..b99de5c 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -71,7 +71,7 @@
import com.android.server.display.config.RefreshRateZone;
import com.android.server.display.config.SdrHdrRatioMap;
import com.android.server.display.config.SdrHdrRatioPoint;
-import com.android.server.display.config.SensorDetails;
+import com.android.server.display.config.SensorData;
import com.android.server.display.config.ThermalStatus;
import com.android.server.display.config.ThermalThrottling;
import com.android.server.display.config.ThresholdPoint;
@@ -349,6 +349,20 @@
* <proxSensor>
* <type>android.sensor.proximity</type>
* <name>1234 Proximity Sensor</name>
+ * <refreshRate>
+ * <minimum>60</minimum>
+ * <maximum>60</maximum>
+ * </refreshRate>
+ * <supportedModes>
+ * <point>
+ * <first>60</first> // refreshRate
+ * <second>60</second> //vsyncRate
+ * </point>
+ * <point>
+ * <first>120</first> // refreshRate
+ * <second>120</second> //vsyncRate
+ * </point>
+ * </supportedModes>
* </proxSensor>
*
* <ambientLightHorizonLong>10001</ambientLightHorizonLong>
@@ -581,15 +595,15 @@
private final Context mContext;
// The details of the ambient light sensor associated with this display.
- private final SensorData mAmbientLightSensor = new SensorData();
+ private SensorData mAmbientLightSensor;
// The details of the doze brightness sensor associated with this display.
- private final SensorData mScreenOffBrightnessSensor = new SensorData();
+ private SensorData mScreenOffBrightnessSensor;
// The details of the proximity sensor associated with this display.
// Is null when no sensor should be used for that display
@Nullable
- private SensorData mProximitySensor = new SensorData();
+ private SensorData mProximitySensor;
private final List<RefreshRateLimitation> mRefreshRateLimitations =
new ArrayList<>(2 /*initialCapacity*/);
@@ -1913,9 +1927,10 @@
loadLuxThrottling(config);
loadQuirks(config);
loadBrightnessRamps(config);
- loadAmbientLightSensorFromDdc(config);
- loadScreenOffBrightnessSensorFromDdc(config);
- loadProxSensorFromDdc(config);
+ mAmbientLightSensor = SensorData.loadAmbientLightSensorConfig(config,
+ mContext.getResources());
+ mScreenOffBrightnessSensor = SensorData.loadScreenOffBrightnessSensorConfig(config);
+ mProximitySensor = SensorData.loadProxSensorConfig(config);
loadAmbientHorizonFromDdc(config);
loadBrightnessChangeThresholds(config);
loadAutoBrightnessConfigValues(config);
@@ -1940,9 +1955,9 @@
loadBrightnessConstraintsFromConfigXml();
loadBrightnessMapFromConfigXml();
loadBrightnessRampsFromConfigXml();
- loadAmbientLightSensorFromConfigXml();
+ mAmbientLightSensor = SensorData.loadAmbientLightSensorConfig(mContext.getResources());
+ mProximitySensor = SensorData.loadSensorUnspecifiedConfig();
loadBrightnessChangeThresholdsFromXml();
- setProxSensorUnspecified();
loadAutoBrightnessConfigsFromConfigXml();
loadAutoBrightnessAvailableFromConfigXml();
loadRefreshRateSetting(null);
@@ -1966,8 +1981,8 @@
mBrightnessRampDecreaseMaxIdleMillis = 0;
mBrightnessRampIncreaseMaxIdleMillis = 0;
setSimpleMappingStrategyValues();
- loadAmbientLightSensorFromConfigXml();
- setProxSensorUnspecified();
+ mAmbientLightSensor = SensorData.loadAmbientLightSensorConfig(mContext.getResources());
+ mProximitySensor = SensorData.loadSensorUnspecifiedConfig();
loadAutoBrightnessAvailableFromConfigXml();
}
@@ -2919,64 +2934,10 @@
mBrightnessRampSlowDecrease = mBrightnessRampSlowIncrease;
}
- private void loadAmbientLightSensorFromConfigXml() {
- mAmbientLightSensor.name = "";
- mAmbientLightSensor.type = mContext.getResources().getString(
- com.android.internal.R.string.config_displayLightSensorType);
- }
-
private void loadAutoBrightnessConfigsFromConfigXml() {
loadAutoBrightnessDisplayBrightnessMapping(null /*AutoBrightnessConfig*/);
}
- private void loadAmbientLightSensorFromDdc(DisplayConfiguration config) {
- final SensorDetails sensorDetails = config.getLightSensor();
- if (sensorDetails != null) {
- loadSensorData(sensorDetails, mAmbientLightSensor);
- } else {
- loadAmbientLightSensorFromConfigXml();
- }
- }
-
- private void setProxSensorUnspecified() {
- mProximitySensor = new SensorData();
- }
-
- private void loadScreenOffBrightnessSensorFromDdc(DisplayConfiguration config) {
- final SensorDetails sensorDetails = config.getScreenOffBrightnessSensor();
- if (sensorDetails != null) {
- loadSensorData(sensorDetails, mScreenOffBrightnessSensor);
- }
- }
-
- private void loadProxSensorFromDdc(DisplayConfiguration config) {
- SensorDetails sensorDetails = config.getProxSensor();
- if (sensorDetails != null) {
- String name = sensorDetails.getName();
- String type = sensorDetails.getType();
- if ("".equals(name) && "".equals(type)) {
- // <proxSensor> with empty values to the config means no sensor should be used
- mProximitySensor = null;
- } else {
- mProximitySensor = new SensorData();
- loadSensorData(sensorDetails, mProximitySensor);
- }
- } else {
- setProxSensorUnspecified();
- }
- }
-
- private void loadSensorData(@NonNull SensorDetails sensorDetails,
- @NonNull SensorData sensorData) {
- sensorData.name = sensorDetails.getName();
- sensorData.type = sensorDetails.getType();
- final RefreshRateRange rr = sensorDetails.getRefreshRate();
- if (rr != null) {
- sensorData.minRefreshRate = rr.getMinimum().floatValue();
- sensorData.maxRefreshRate = rr.getMaximum().floatValue();
- }
- }
-
private void loadBrightnessChangeThresholdsFromXml() {
loadBrightnessChangeThresholds(/* config= */ null);
}
@@ -3390,37 +3351,6 @@
}
/**
- * Uniquely identifies a Sensor, with the combination of Type and Name.
- */
- public static class SensorData {
- public String type;
- public String name;
- public float minRefreshRate = 0.0f;
- public float maxRefreshRate = Float.POSITIVE_INFINITY;
-
- @Override
- public String toString() {
- return "Sensor{"
- + "type: " + type
- + ", name: " + name
- + ", refreshRateRange: [" + minRefreshRate + ", " + maxRefreshRate + "]"
- + "} ";
- }
-
- /**
- * @return True if the sensor matches both the specified name and type, or one if only one
- * is specified (not-empty). Always returns false if both parameters are null or empty.
- */
- public boolean matches(String sensorName, String sensorType) {
- final boolean isNameSpecified = !TextUtils.isEmpty(sensorName);
- final boolean isTypeSpecified = !TextUtils.isEmpty(sensorType);
- return (isNameSpecified || isTypeSpecified)
- && (!isNameSpecified || sensorName.equals(name))
- && (!isTypeSpecified || sensorType.equals(type));
- }
- }
-
- /**
* Container for high brightness mode configuration data.
*/
static class HighBrightnessModeData {
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index eae153c..8046dbf 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -158,7 +158,7 @@
import com.android.server.SystemService;
import com.android.server.UiThread;
import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
-import com.android.server.display.DisplayDeviceConfig.SensorData;
+import com.android.server.display.config.SensorData;
import com.android.server.display.feature.DeviceConfigParameterProvider;
import com.android.server.display.feature.DisplayManagerFlags;
import com.android.server.display.layout.Layout;
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index d8ac52e..5761c31 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1540,12 +1540,13 @@
performScreenOffTransition = true;
break;
case DisplayPowerRequest.POLICY_DOZE:
- if (mPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) {
+ if (mDozeStateOverride != Display.STATE_UNKNOWN) {
+ state = mDozeStateOverride;
+ } else if (mPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) {
state = mPowerRequest.dozeScreenState;
} else {
state = Display.STATE_DOZE;
}
- state = mDozeStateOverride == Display.STATE_UNKNOWN ? state : mDozeStateOverride;
if (!mAllowAutoBrightnessWhileDozingConfig) {
brightnessState = mPowerRequest.dozeScreenBrightness;
mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE);
diff --git a/services/core/java/com/android/server/display/config/SensorData.java b/services/core/java/com/android/server/display/config/SensorData.java
new file mode 100644
index 0000000..3bb35bf
--- /dev/null
+++ b/services/core/java/com/android/server/display/config/SensorData.java
@@ -0,0 +1,184 @@
+/*
+ * 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.server.display.config;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Uniquely identifies a Sensor, with the combination of Type and Name.
+ */
+public class SensorData {
+
+ @Nullable
+ public final String type;
+ @Nullable
+ public final String name;
+ public final float minRefreshRate;
+ public final float maxRefreshRate;
+ public final List<SupportedMode> supportedModes;
+
+ @VisibleForTesting
+ public SensorData() {
+ this(/* type= */ null, /* name= */ null);
+ }
+
+ @VisibleForTesting
+ public SensorData(String type, String name) {
+ this(type, name, /* minRefreshRate= */ 0f, /* maxRefreshRate= */ Float.POSITIVE_INFINITY);
+ }
+
+ @VisibleForTesting
+ public SensorData(String type, String name, float minRefreshRate, float maxRefreshRate) {
+ this(type, name, minRefreshRate, maxRefreshRate, /* supportedModes= */ List.of());
+ }
+
+ @VisibleForTesting
+ public SensorData(String type, String name, float minRefreshRate, float maxRefreshRate,
+ List<SupportedMode> supportedModes) {
+ this.type = type;
+ this.name = name;
+ this.minRefreshRate = minRefreshRate;
+ this.maxRefreshRate = maxRefreshRate;
+ this.supportedModes = Collections.unmodifiableList(supportedModes);
+ }
+
+ /**
+ * @return True if the sensor matches both the specified name and type, or one if only one
+ * is specified (not-empty). Always returns false if both parameters are null or empty.
+ */
+ public boolean matches(String sensorName, String sensorType) {
+ final boolean isNameSpecified = !TextUtils.isEmpty(sensorName);
+ final boolean isTypeSpecified = !TextUtils.isEmpty(sensorType);
+ return (isNameSpecified || isTypeSpecified)
+ && (!isNameSpecified || sensorName.equals(name))
+ && (!isTypeSpecified || sensorType.equals(type));
+ }
+
+ @Override
+ public String toString() {
+ return "SensorData{"
+ + "type= " + type
+ + ", name= " + name
+ + ", refreshRateRange: [" + minRefreshRate + ", " + maxRefreshRate + "]"
+ + ", supportedModes=" + supportedModes
+ + '}';
+ }
+
+ /**
+ * Loads ambient light sensor data from DisplayConfiguration and if missing from resources xml
+ */
+ public static SensorData loadAmbientLightSensorConfig(DisplayConfiguration config,
+ Resources resources) {
+ SensorDetails sensorDetails = config.getLightSensor();
+ if (sensorDetails != null) {
+ return loadSensorData(sensorDetails);
+ } else {
+ return loadAmbientLightSensorConfig(resources);
+ }
+ }
+
+ /**
+ * Loads ambient light sensor data from resources xml
+ */
+ public static SensorData loadAmbientLightSensorConfig(Resources resources) {
+ return new SensorData(
+ resources.getString(com.android.internal.R.string.config_displayLightSensorType),
+ /* name= */ "");
+ }
+
+ /**
+ * Loads screen off brightness sensor data from DisplayConfiguration
+ */
+ public static SensorData loadScreenOffBrightnessSensorConfig(DisplayConfiguration config) {
+ SensorDetails sensorDetails = config.getScreenOffBrightnessSensor();
+ if (sensorDetails != null) {
+ return loadSensorData(sensorDetails);
+ } else {
+ return new SensorData();
+ }
+ }
+
+ /**
+ * Loads proximity sensor data from DisplayConfiguration
+ */
+ @Nullable
+ public static SensorData loadProxSensorConfig(DisplayConfiguration config) {
+ SensorDetails sensorDetails = config.getProxSensor();
+ if (sensorDetails != null) {
+ String name = sensorDetails.getName();
+ String type = sensorDetails.getType();
+ if ("".equals(name) && "".equals(type)) {
+ // <proxSensor> with empty values to the config means no sensor should be used.
+ // See also {@link com.android.server.display.utils.SensorUtils}
+ return null;
+ } else {
+ return loadSensorData(sensorDetails);
+ }
+ } else {
+ return new SensorData();
+ }
+ }
+
+ /**
+ * Loads sensor unspecified config, this means system should use default sensor.
+ * See also {@link com.android.server.display.utils.SensorUtils}
+ */
+ @NonNull
+ public static SensorData loadSensorUnspecifiedConfig() {
+ return new SensorData();
+ }
+
+ private static SensorData loadSensorData(@NonNull SensorDetails sensorDetails) {
+ float minRefreshRate = 0f;
+ float maxRefreshRate = Float.POSITIVE_INFINITY;
+ RefreshRateRange rr = sensorDetails.getRefreshRate();
+ if (rr != null) {
+ minRefreshRate = rr.getMinimum().floatValue();
+ maxRefreshRate = rr.getMaximum().floatValue();
+ }
+ ArrayList<SupportedMode> supportedModes = new ArrayList<>();
+ NonNegativeFloatToFloatMap configSupportedModes = sensorDetails.getSupportedModes();
+ if (configSupportedModes != null) {
+ for (NonNegativeFloatToFloatPoint supportedMode : configSupportedModes.getPoint()) {
+ supportedModes.add(new SupportedMode(supportedMode.getFirst().floatValue(),
+ supportedMode.getSecond().floatValue()));
+ }
+ }
+
+ return new SensorData(sensorDetails.getType(), sensorDetails.getName(), minRefreshRate,
+ maxRefreshRate, supportedModes);
+ }
+
+ public static class SupportedMode {
+ public final float refreshRate;
+ public final float vsyncRate;
+
+ public SupportedMode(float refreshRate, float vsyncRate) {
+ this.refreshRate = refreshRate;
+ this.vsyncRate = vsyncRate;
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/display/state/DisplayStateController.java b/services/core/java/com/android/server/display/state/DisplayStateController.java
index 5d6e650..5f28934 100644
--- a/services/core/java/com/android/server/display/state/DisplayStateController.java
+++ b/services/core/java/com/android/server/display/state/DisplayStateController.java
@@ -61,12 +61,13 @@
mPerformScreenOffTransition = true;
break;
case DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE:
- if (displayPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) {
+ if (mDozeStateOverride != Display.STATE_UNKNOWN) {
+ state = mDozeStateOverride;
+ } else if (displayPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) {
state = displayPowerRequest.dozeScreenState;
} else {
state = Display.STATE_DOZE;
}
- state = mDozeStateOverride == Display.STATE_UNKNOWN ? state : mDozeStateOverride;
break;
case DisplayManagerInternal.DisplayPowerRequest.POLICY_DIM:
case DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT:
diff --git a/services/core/java/com/android/server/display/utils/SensorUtils.java b/services/core/java/com/android/server/display/utils/SensorUtils.java
index 56321cd..8b9fe108 100644
--- a/services/core/java/com/android/server/display/utils/SensorUtils.java
+++ b/services/core/java/com/android/server/display/utils/SensorUtils.java
@@ -21,7 +21,7 @@
import android.hardware.SensorManager;
import android.text.TextUtils;
-import com.android.server.display.DisplayDeviceConfig;
+import com.android.server.display.config.SensorData;
import java.util.List;
@@ -36,7 +36,7 @@
*/
@Nullable
public static Sensor findSensor(@Nullable SensorManager sensorManager,
- @Nullable DisplayDeviceConfig.SensorData sensorData, int fallbackType) {
+ @Nullable SensorData sensorData, int fallbackType) {
if (sensorData == null) {
return null;
} else {
diff --git a/services/core/java/com/android/server/net/NetworkManagementService.java b/services/core/java/com/android/server/net/NetworkManagementService.java
index 550ad5d..681d1a0 100644
--- a/services/core/java/com/android/server/net/NetworkManagementService.java
+++ b/services/core/java/com/android/server/net/NetworkManagementService.java
@@ -74,7 +74,6 @@
import com.android.internal.util.DumpUtils;
import com.android.internal.util.HexDump;
import com.android.modules.utils.build.SdkLevel;
-import com.android.net.flags.Flags;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.server.FgThread;
@@ -328,10 +327,10 @@
/**
* Notify our observers of a change in the data activity state of the interface
*/
- private void notifyInterfaceClassActivity(int type, boolean isActive, long tsNanos,
+ private void notifyInterfaceClassActivity(int label, boolean isActive, long tsNanos,
int uid) {
invokeForAllObservers(o -> o.interfaceClassDataActivityChanged(
- type, isActive, tsNanos, uid));
+ label, isActive, tsNanos, uid));
}
// Sync the state of the given chain with the native daemon.
@@ -1062,7 +1061,7 @@
}
Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "setDataSaverModeEnabled");
try {
- if (Flags.setDataSaverViaCm()) {
+ if (SdkLevel.isAtLeastV()) {
// setDataSaverEnabled throws if it fails to set data saver.
mContext.getSystemService(ConnectivityManager.class)
.setDataSaverEnabled(enable);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 8c75367..7e51526 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -706,6 +706,7 @@
private boolean mNotificationEffectsEnabledForAutomotive;
private DeviceConfig.OnPropertiesChangedListener mDeviceConfigChangedListener;
protected NotificationAttentionHelper mAttentionHelper;
+ private boolean mFlagRefactorAttentionHelper;
private int mWarnRemoteViewsSizeBytes;
private int mStripRemoteViewsSizeBytes;
@@ -1189,7 +1190,7 @@
@Override
public void onSetDisabled(int status) {
synchronized (mNotificationLock) {
- if (Flags.refactorAttentionHelper()) {
+ if (mFlagRefactorAttentionHelper) {
mAttentionHelper.updateDisableNotificationEffectsLocked(status);
} else {
mDisableNotificationEffects =
@@ -1335,7 +1336,7 @@
public void clearEffects() {
synchronized (mNotificationLock) {
if (DBG) Slog.d(TAG, "clearEffects");
- if (Flags.refactorAttentionHelper()) {
+ if (mFlagRefactorAttentionHelper) {
mAttentionHelper.clearAttentionEffects();
} else {
clearSoundLocked();
@@ -1564,7 +1565,7 @@
int changedFlags = data.getFlags() ^ flags;
if ((changedFlags & FLAG_SUPPRESS_NOTIFICATION) != 0) {
// Suppress notification flag changed, clear any effects
- if (Flags.refactorAttentionHelper()) {
+ if (mFlagRefactorAttentionHelper) {
mAttentionHelper.clearEffectsLocked(key);
} else {
clearEffectsLocked(key);
@@ -1913,7 +1914,7 @@
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
- if (!Flags.refactorAttentionHelper()) {
+ if (!mFlagRefactorAttentionHelper) {
if (action.equals(Intent.ACTION_SCREEN_ON)) {
// Keep track of screen on/off state, but do not turn off the notification light
// until user passes through the lock screen or views the notification.
@@ -2029,7 +2030,7 @@
ContentResolver resolver = getContext().getContentResolver();
resolver.registerContentObserver(NOTIFICATION_BADGING_URI,
false, this, UserHandle.USER_ALL);
- if (!Flags.refactorAttentionHelper()) {
+ if (!mFlagRefactorAttentionHelper) {
resolver.registerContentObserver(NOTIFICATION_LIGHT_PULSE_URI,
false, this, UserHandle.USER_ALL);
}
@@ -2059,7 +2060,7 @@
public void update(Uri uri) {
ContentResolver resolver = getContext().getContentResolver();
- if (!Flags.refactorAttentionHelper()) {
+ if (!mFlagRefactorAttentionHelper) {
if (uri == null || NOTIFICATION_LIGHT_PULSE_URI.equals(uri)) {
boolean pulseEnabled = Settings.System.getIntForUser(resolver,
Settings.System.NOTIFICATION_LIGHT_PULSE, 0, UserHandle.USER_CURRENT)
@@ -2560,7 +2561,9 @@
mToastRateLimiter = toastRateLimiter;
- if (Flags.refactorAttentionHelper()) {
+ //Cache aconfig flag value
+ mFlagRefactorAttentionHelper = Flags.refactorAttentionHelper();
+ if (mFlagRefactorAttentionHelper) {
mAttentionHelper = new NotificationAttentionHelper(getContext(), lightsManager,
mAccessibilityManager, mPackageManagerClient, userManager, usageStats,
mNotificationManagerPrivate, mZenModeHelper, flagResolver);
@@ -2570,7 +2573,7 @@
// If this is called within a test, make sure to unregister the intent receivers by
// calling onDestroy()
IntentFilter filter = new IntentFilter();
- if (!Flags.refactorAttentionHelper()) {
+ if (!mFlagRefactorAttentionHelper) {
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
@@ -2898,7 +2901,7 @@
}
registerNotificationPreferencesPullers();
new LockPatternUtils(getContext()).registerStrongAuthTracker(mStrongAuthTracker);
- if (Flags.refactorAttentionHelper()) {
+ if (mFlagRefactorAttentionHelper) {
mAttentionHelper.onSystemReady();
}
} else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
@@ -6571,7 +6574,7 @@
pw.println(" mMaxPackageEnqueueRate=" + mMaxPackageEnqueueRate);
pw.println(" hideSilentStatusBar="
+ mPreferencesHelper.shouldHideSilentStatusIcons());
- if (Flags.refactorAttentionHelper()) {
+ if (mFlagRefactorAttentionHelper) {
mAttentionHelper.dump(pw, " ", filter);
}
}
@@ -7042,9 +7045,8 @@
channelId = (new Notification.TvExtender(notification)).getChannelId();
}
String shortcutId = n.getShortcutId();
- final NotificationChannel channel = mPreferencesHelper.getConversationNotificationChannel(
- pkg, notificationUid, channelId, shortcutId,
- true /* parent ok */, false /* includeDeleted */);
+ final NotificationChannel channel = getNotificationChannelRestoreDeleted(pkg,
+ callingUid, notificationUid, channelId, shortcutId);
if (channel == null) {
final String noChannelStr = "No Channel found for "
+ "pkg=" + pkg
@@ -7162,6 +7164,35 @@
return true;
}
+ /**
+ * Returns a channel, if exists, and restores deleted conversation channels.
+ */
+ @Nullable
+ private NotificationChannel getNotificationChannelRestoreDeleted(String pkg,
+ int callingUid, int notificationUid, String channelId, String conversationId) {
+ // Restore a deleted conversation channel, if exists. Otherwise use the parent channel.
+ NotificationChannel channel = mPreferencesHelper.getConversationNotificationChannel(
+ pkg, notificationUid, channelId, conversationId,
+ true /* parent ok */, !TextUtils.isEmpty(conversationId) /* includeDeleted */);
+ // Restore deleted conversation channel
+ if (channel != null && channel.isDeleted()) {
+ if (Objects.equals(conversationId, channel.getConversationId())) {
+ boolean needsPolicyFileChange = mPreferencesHelper.createNotificationChannel(
+ pkg, notificationUid, channel, true /* fromTargetApp */,
+ mConditionProviders.isPackageOrComponentAllowed(pkg,
+ UserHandle.getUserId(notificationUid)), callingUid, true);
+ // Update policy file if the conversation channel was restored
+ if (needsPolicyFileChange) {
+ handleSavePolicyFile();
+ }
+ } else {
+ // Do not restore parent channel
+ channel = null;
+ }
+ }
+ return channel;
+ }
+
private void onConversationRemovedInternal(String pkg, int uid, Set<String> shortcuts) {
checkCallerIsSystem();
Preconditions.checkStringNotEmpty(pkg);
@@ -7844,7 +7875,7 @@
boolean wasPosted = removeFromNotificationListsLocked(r);
cancelNotificationLocked(r, false, REASON_SNOOZED, wasPosted, null,
SystemClock.elapsedRealtime());
- if (Flags.refactorAttentionHelper()) {
+ if (mFlagRefactorAttentionHelper) {
mAttentionHelper.updateLightsLocked();
} else {
updateLightsLocked();
@@ -7984,7 +8015,7 @@
cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName,
mSendDelete, childrenFlagChecker, mReason,
mCancellationElapsedTimeMs);
- if (Flags.refactorAttentionHelper()) {
+ if (mFlagRefactorAttentionHelper) {
mAttentionHelper.updateLightsLocked();
} else {
updateLightsLocked();
@@ -8281,7 +8312,7 @@
int buzzBeepBlinkLoggingCode = 0;
if (!r.isHidden()) {
- if (Flags.refactorAttentionHelper()) {
+ if (mFlagRefactorAttentionHelper) {
buzzBeepBlinkLoggingCode = mAttentionHelper.buzzBeepBlinkLocked(r,
new NotificationAttentionHelper.Signals(
mUserProfiles.isCurrentProfile(r.getUserId()),
@@ -9268,7 +9299,7 @@
|| interruptiveChanged;
if (interceptBefore && !record.isIntercepted()
&& record.isNewEnoughForAlerting(System.currentTimeMillis())) {
- if (Flags.refactorAttentionHelper()) {
+ if (mFlagRefactorAttentionHelper) {
mAttentionHelper.buzzBeepBlinkLocked(record,
new NotificationAttentionHelper.Signals(
mUserProfiles.isCurrentProfile(record.getUserId()), mListenerHints));
@@ -9648,7 +9679,7 @@
});
}
- if (Flags.refactorAttentionHelper()) {
+ if (mFlagRefactorAttentionHelper) {
mAttentionHelper.clearEffectsLocked(canceledKey);
} else {
// sound
@@ -10012,7 +10043,7 @@
cancellationElapsedTimeMs);
}
}
- if (Flags.refactorAttentionHelper()) {
+ if (mFlagRefactorAttentionHelper) {
mAttentionHelper.updateLightsLocked();
} else {
updateLightsLocked();
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 8bf903a..f45571a 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -578,6 +578,12 @@
? null
: ps.getUserStateOrDefault(nextUserId).getArchiveState();
+ // Preserve firstInstallTime in case of DELETE_KEEP_DATA
+ // For full uninstalls, reset firstInstallTime to 0 as if it has never been installed
+ final long firstInstallTime = (flags & DELETE_KEEP_DATA) == 0
+ ? 0
+ : ps.getUserStateOrDefault(nextUserId).getFirstInstallTimeMillis();
+
ps.setUserState(nextUserId,
ps.getCeDataInode(nextUserId),
ps.getDeDataInode(nextUserId),
@@ -597,7 +603,7 @@
PackageManager.UNINSTALL_REASON_UNKNOWN,
null /*harmfulAppWarning*/,
null /*splashScreenTheme*/,
- 0 /*firstInstallTime*/,
+ firstInstallTime,
PackageManager.USER_MIN_ASPECT_RATIO_UNSET,
archiveState);
}
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/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 87ae045..43f3209 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -39,7 +39,6 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
-import android.os.IBinder;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.SystemProperties;
@@ -60,7 +59,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.common.ProtoLog;
-import com.android.server.LocalServices;
import com.android.server.wm.utils.InsetUtils;
import java.io.PrintWriter;
@@ -151,97 +149,77 @@
// Don't start any animation for it.
return null;
}
- WindowManagerInternal windowManagerInternal =
- LocalServices.getService(WindowManagerInternal.class);
- IBinder focusedWindowToken = windowManagerInternal.getFocusedWindowToken();
window = wmService.getFocusedWindowLocked();
if (window == null) {
- EmbeddedWindowController.EmbeddedWindow embeddedWindow =
- wmService.mEmbeddedWindowController.getByInputTransferToken(
- focusedWindowToken);
- if (embeddedWindow != null) {
- ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
- "Current focused window is embeddedWindow. Dispatch KEYCODE_BACK.");
- return null;
- }
- }
-
- // Lets first gather the states of things
- // - What is our current window ?
- // - Does it has an Activity and a Task ?
- // TODO Temp workaround for Sysui until b/221071505 is fixed
- if (window != null) {
- ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
- "Focused window found using getFocusedWindowToken");
- }
-
- if (window != null) {
- // This is needed to bridge the old and new back behavior with recents. While in
- // Overview with live tile enabled, the previous app is technically focused but we
- // add an input consumer to capture all input that would otherwise go to the apps
- // being controlled by the animation. This means that the window resolved is not
- // the right window to consume back while in overview, so we need to route it to
- // launcher and use the legacy behavior of injecting KEYCODE_BACK since the existing
- // compat callback in VRI only works when the window is focused.
- // This symptom also happen while shell transition enabled, we can check that by
- // isTransientLaunch to know whether the focus window is point to live tile.
- final RecentsAnimationController recentsAnimationController =
- wmService.getRecentsAnimationController();
- final ActivityRecord ar = window.mActivityRecord;
- if ((ar != null && ar.isActivityTypeHomeOrRecents()
- && ar.mTransitionController.isTransientLaunch(ar))
- || (recentsAnimationController != null
- && recentsAnimationController.shouldApplyInputConsumer(ar))) {
- ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Current focused window being animated by "
- + "recents. Overriding back callback to recents controller callback.");
- return null;
- }
-
- if (!window.isDrawn()) {
- ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
- "Focused window didn't have a valid surface drawn.");
- return null;
- }
- }
-
- if (window == null) {
// We don't have any focused window, fallback ont the top currentTask of the focused
// display.
ProtoLog.w(WM_DEBUG_BACK_PREVIEW,
"No focused window, defaulting to top current task's window");
currentTask = wmService.mAtmService.getTopDisplayFocusedRootTask();
- window = currentTask.getWindow(WindowState::isFocused);
+ window = currentTask != null
+ ? currentTask.getWindow(WindowState::isFocused) : null;
}
- // Now let's find if this window has a callback from the client side.
- OnBackInvokedCallbackInfo callbackInfo = null;
- if (window != null) {
- currentActivity = window.mActivityRecord;
- currentTask = window.getTask();
- callbackInfo = window.getOnBackInvokedCallbackInfo();
- if (callbackInfo == null) {
- Slog.e(TAG, "No callback registered, returning null.");
- return null;
- }
- if (!callbackInfo.isSystemCallback()) {
- backType = BackNavigationInfo.TYPE_CALLBACK;
- }
- infoBuilder.setOnBackInvokedCallback(callbackInfo.getCallback());
- infoBuilder.setAnimationCallback(callbackInfo.isAnimationCallback());
- mNavigationMonitor.startMonitor(window, navigationObserver);
- }
-
- ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "startBackNavigation currentTask=%s, "
- + "topRunningActivity=%s, callbackInfo=%s, currentFocus=%s",
- currentTask, currentActivity, callbackInfo, window);
-
if (window == null) {
Slog.e(TAG, "Window is null, returning null.");
return null;
}
+ // This is needed to bridge the old and new back behavior with recents. While in
+ // Overview with live tile enabled, the previous app is technically focused but we
+ // add an input consumer to capture all input that would otherwise go to the apps
+ // being controlled by the animation. This means that the window resolved is not
+ // the right window to consume back while in overview, so we need to route it to
+ // launcher and use the legacy behavior of injecting KEYCODE_BACK since the existing
+ // compat callback in VRI only works when the window is focused.
+ // This symptom also happen while shell transition enabled, we can check that by
+ // isTransientLaunch to know whether the focus window is point to live tile.
+ final RecentsAnimationController recentsAnimationController =
+ wmService.getRecentsAnimationController();
+ final ActivityRecord tmpAR = window.mActivityRecord;
+ if ((tmpAR != null && tmpAR.isActivityTypeHomeOrRecents()
+ && tmpAR.mTransitionController.isTransientLaunch(tmpAR))
+ || (recentsAnimationController != null
+ && recentsAnimationController.shouldApplyInputConsumer(tmpAR))) {
+ ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Current focused window being animated by "
+ + "recents. Overriding back callback to recents controller callback.");
+ return null;
+ }
+
+ if (!window.isDrawn()) {
+ ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
+ "Focused window didn't have a valid surface drawn.");
+ return null;
+ }
+
+ currentActivity = window.mActivityRecord;
+ currentTask = window.getTask();
+ if ((currentTask != null && !currentTask.isVisibleRequested())
+ || (currentActivity != null && !currentActivity.isVisibleRequested())) {
+ // Closing transition is happening on focus window and should be update soon,
+ // don't drive back navigation with it.
+ ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Focus window is closing.");
+ return null;
+ }
+ // Now let's find if this window has a callback from the client side.
+ final OnBackInvokedCallbackInfo callbackInfo = window.getOnBackInvokedCallbackInfo();
+ if (callbackInfo == null) {
+ Slog.e(TAG, "No callback registered, returning null.");
+ return null;
+ }
+ if (!callbackInfo.isSystemCallback()) {
+ backType = BackNavigationInfo.TYPE_CALLBACK;
+ }
+ infoBuilder.setOnBackInvokedCallback(callbackInfo.getCallback());
+ infoBuilder.setAnimationCallback(callbackInfo.isAnimationCallback());
+ mNavigationMonitor.startMonitor(window, navigationObserver);
+
+ ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "startBackNavigation currentTask=%s, "
+ + "topRunningActivity=%s, callbackInfo=%s, currentFocus=%s",
+ currentTask, currentActivity, callbackInfo, window);
+
// If we don't need to set up the animation, we return early. This is the case when
// - We have an application callback.
// - We don't have any ActivityRecord or Task to animate.
@@ -322,12 +300,13 @@
}
return false;
}, currentTask, false /*includeBoundary*/, true /*traverseTopToBottom*/);
- final ActivityRecord tmpPre = prevTask.getTopNonFinishingActivity();
+ final ActivityRecord tmpPre = prevTask != null
+ ? prevTask.getTopNonFinishingActivity() : null;
if (tmpPre != null) {
prevActivities.add(tmpPre);
findAdjacentActivityIfExist(tmpPre, prevActivities);
}
- if (prevActivities.isEmpty()
+ if (prevTask == null || prevActivities.isEmpty()
|| (isOccluded && !prevActivities.get(0).canShowWhenLocked())) {
backType = BackNavigationInfo.TYPE_CALLBACK;
} else if (prevTask.isActivityTypeHome()) {
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 215934f..cca4261 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -455,6 +455,20 @@
<xs:annotation name="nullable"/>
<xs:annotation name="final"/>
</xs:element>
+ <!-- list of supported modes when sensor is ON. Each point corresponds to one mode.
+ Mode format is : first = refreshRate, second = vsyncRate. E.g. :
+ <supportedModes>
+ <point>
+ <first>60</first> // refreshRate
+ <second>60</second> //vsyncRate
+ </point>
+ ....
+ </supportedModes>
+ -->
+ <xs:element type="nonNegativeFloatToFloatMap" name="supportedModes" minOccurs="0">
+ <xs:annotation name="nullable"/>
+ <xs:annotation name="final"/>
+ </xs:element>
</xs:sequence>
</xs:complexType>
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index f7e0043..f767291 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -349,9 +349,11 @@
ctor public SensorDetails();
method @Nullable public final String getName();
method @Nullable public final com.android.server.display.config.RefreshRateRange getRefreshRate();
+ method @Nullable public final com.android.server.display.config.NonNegativeFloatToFloatMap getSupportedModes();
method @Nullable public final String getType();
method public final void setName(@Nullable String);
method public final void setRefreshRate(@Nullable com.android.server.display.config.RefreshRateRange);
+ method public final void setSupportedModes(@Nullable com.android.server.display.config.NonNegativeFloatToFloatMap);
method public final void setType(@Nullable String);
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 179a9d5..0bcbeb9 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -17,9 +17,12 @@
package com.android.server.display;
+import static com.android.server.display.config.SensorData.SupportedMode;
import static com.android.server.display.utils.DeviceConfigParsingUtils.ambientBrightnessThresholdsIntToFloat;
import static com.android.server.display.utils.DeviceConfigParsingUtils.displayBrightnessThresholdsIntToFloat;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -526,6 +529,26 @@
}
@Test
+ public void testProximitySensorWithRefreshRatesFromDisplayConfig() throws IOException {
+ setupDisplayDeviceConfigFromDisplayConfigFile(
+ getContent(getValidLuxThrottling(), getValidProxSensorWithRefreshRateAndVsyncRate(),
+ /* includeIdleMode= */ true));
+ assertEquals("test_proximity_sensor",
+ mDisplayDeviceConfig.getProximitySensor().type);
+ assertEquals("Test Proximity Sensor",
+ mDisplayDeviceConfig.getProximitySensor().name);
+ assertEquals(mDisplayDeviceConfig.getProximitySensor().minRefreshRate, 60, SMALL_DELTA);
+ assertEquals(mDisplayDeviceConfig.getProximitySensor().maxRefreshRate, 90, SMALL_DELTA);
+ assertThat(mDisplayDeviceConfig.getProximitySensor().supportedModes).hasSize(2);
+ SupportedMode mode = mDisplayDeviceConfig.getProximitySensor().supportedModes.get(0);
+ assertEquals(mode.refreshRate, 60, SMALL_DELTA);
+ assertEquals(mode.vsyncRate, 65, SMALL_DELTA);
+ mode = mDisplayDeviceConfig.getProximitySensor().supportedModes.get(1);
+ assertEquals(mode.refreshRate, 120, SMALL_DELTA);
+ assertEquals(mode.vsyncRate, 125, SMALL_DELTA);
+ }
+
+ @Test
public void testBlockingZoneThresholdsFromDisplayConfig() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile();
@@ -821,6 +844,27 @@
+ "</proxSensor>\n";
}
+ private String getValidProxSensorWithRefreshRateAndVsyncRate() {
+ return "<proxSensor>\n"
+ + "<type>test_proximity_sensor</type>\n"
+ + "<name>Test Proximity Sensor</name>\n"
+ + "<refreshRate>\n"
+ + "<minimum>60</minimum>\n"
+ + "<maximum>90</maximum>\n"
+ + "</refreshRate>\n"
+ + "<supportedModes>\n"
+ + "<point>\n"
+ + "<first>60</first>\n" // refreshRate
+ + "<second>65</second>\n" //vsyncRate
+ + "</point>\n"
+ + "<point>\n"
+ + "<first>120</first>\n" // refreshRate
+ + "<second>125</second>\n" //vsyncRate
+ + "</point>\n"
+ + "</supportedModes>"
+ + "</proxSensor>\n";
+ }
+
private String getProxSensorWithEmptyValues() {
return "<proxSensor>\n"
+ "<type></type>\n"
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 9684f42..3775ac94 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -123,6 +123,7 @@
import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import com.android.server.display.DisplayManagerService.DeviceStateListener;
import com.android.server.display.DisplayManagerService.SyncRoot;
+import com.android.server.display.config.SensorData;
import com.android.server.display.feature.DisplayManagerFlags;
import com.android.server.display.notifications.DisplayNotificationManager;
import com.android.server.input.InputManagerInternal;
@@ -2317,11 +2318,8 @@
String testSensorType = "testType";
Sensor testSensor = TestUtils.createSensor(testSensorType, testSensorName);
- DisplayDeviceConfig.SensorData sensorData = new DisplayDeviceConfig.SensorData();
- sensorData.type = testSensorType;
- sensorData.name = testSensorName;
- sensorData.minRefreshRate = 10f;
- sensorData.maxRefreshRate = 100f;
+ SensorData sensorData = new SensorData(testSensorType, testSensorName,
+ /* minRefreshRate= */ 10f, /* maxRefreshRate= */ 100f);
when(mMockDisplayDeviceConfig.getProximitySensor()).thenReturn(sensorData);
when(mSensorManager.getSensorList(Sensor.TYPE_ALL)).thenReturn(Collections.singletonList(
@@ -2352,12 +2350,6 @@
String testSensorType = "testType";
Sensor testSensor = TestUtils.createSensor(testSensorType, testSensorName);
- DisplayDeviceConfig.SensorData sensorData = new DisplayDeviceConfig.SensorData();
- sensorData.type = testSensorType;
- sensorData.name = testSensorName;
- sensorData.minRefreshRate = 10f;
- sensorData.maxRefreshRate = 100f;
-
when(mMockDisplayDeviceConfig.getProximitySensor()).thenReturn(null);
when(mSensorManager.getSensorList(Sensor.TYPE_ALL)).thenReturn(Collections.singletonList(
testSensor));
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
index 47521d1..57f392a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -77,6 +77,7 @@
import com.android.server.display.brightness.clamper.BrightnessClamperController;
import com.android.server.display.brightness.clamper.HdrClamper;
import com.android.server.display.color.ColorDisplayService;
+import com.android.server.display.config.SensorData;
import com.android.server.display.feature.DisplayManagerFlags;
import com.android.server.display.feature.flags.Flags;
import com.android.server.display.layout.Layout;
@@ -1618,23 +1619,13 @@
when(displayDeviceMock.getUniqueId()).thenReturn(uniqueId);
when(displayDeviceMock.getDisplayDeviceConfig()).thenReturn(displayDeviceConfigMock);
when(displayDeviceConfigMock.getProximitySensor()).thenReturn(
- new DisplayDeviceConfig.SensorData() {
- {
- type = Sensor.STRING_TYPE_PROXIMITY;
- name = null;
- }
- });
+ new SensorData(Sensor.STRING_TYPE_PROXIMITY, null));
when(displayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
when(displayDeviceConfigMock.isAutoBrightnessAvailable()).thenReturn(true);
when(displayDeviceConfigMock.getAmbientLightSensor()).thenReturn(
- new DisplayDeviceConfig.SensorData());
+ new SensorData());
when(displayDeviceConfigMock.getScreenOffBrightnessSensor()).thenReturn(
- new DisplayDeviceConfig.SensorData() {
- {
- type = Sensor.STRING_TYPE_LIGHT;
- name = null;
- }
- });
+ new SensorData(Sensor.STRING_TYPE_LIGHT, null));
when(displayDeviceConfigMock.getScreenOffBrightnessSensorValueToLux())
.thenReturn(new int[0]);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 37ee23f..9617bd0 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -76,6 +76,7 @@
import com.android.server.display.RampAnimator.DualRampAnimator;
import com.android.server.display.brightness.BrightnessEvent;
import com.android.server.display.color.ColorDisplayService;
+import com.android.server.display.config.SensorData;
import com.android.server.display.feature.DisplayManagerFlags;
import com.android.server.display.feature.flags.Flags;
import com.android.server.display.layout.Layout;
@@ -1515,23 +1516,13 @@
when(displayDeviceMock.getUniqueId()).thenReturn(uniqueId);
when(displayDeviceMock.getDisplayDeviceConfig()).thenReturn(displayDeviceConfigMock);
when(displayDeviceConfigMock.getProximitySensor()).thenReturn(
- new DisplayDeviceConfig.SensorData() {
- {
- type = Sensor.STRING_TYPE_PROXIMITY;
- name = null;
- }
- });
+ new SensorData(Sensor.STRING_TYPE_PROXIMITY, null));
when(displayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
when(displayDeviceConfigMock.isAutoBrightnessAvailable()).thenReturn(true);
when(displayDeviceConfigMock.getAmbientLightSensor()).thenReturn(
- new DisplayDeviceConfig.SensorData());
+ new SensorData());
when(displayDeviceConfigMock.getScreenOffBrightnessSensor()).thenReturn(
- new DisplayDeviceConfig.SensorData() {
- {
- type = Sensor.STRING_TYPE_LIGHT;
- name = null;
- }
- });
+ new SensorData(Sensor.STRING_TYPE_LIGHT, null));
when(displayDeviceConfigMock.getScreenOffBrightnessSensorValueToLux())
.thenReturn(new int[0]);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
index 534a708..ebd6614 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
@@ -37,6 +37,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.server.display.config.SensorData;
import com.android.server.testutils.OffsettableClock;
import org.junit.Before;
@@ -74,14 +75,7 @@
mClock = new OffsettableClock.Stopped();
mTestLooper = new TestLooper(mClock::now);
when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(
- new DisplayDeviceConfig.SensorData() {
- {
- type = Sensor.STRING_TYPE_PROXIMITY;
- // This is kept null because currently there is no way to define a sensor
- // name in TestUtils
- name = null;
- }
- });
+ new SensorData(Sensor.STRING_TYPE_PROXIMITY, null));
setUpProxSensor();
DisplayPowerProximityStateController.Injector injector =
new DisplayPowerProximityStateController.Injector() {
@@ -171,13 +165,7 @@
@Test
public void isProximitySensorAvailableReturnsFalseWhenNotAvailableAndNoDefault() {
- when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(
- new DisplayDeviceConfig.SensorData() {
- {
- type = null;
- name = null;
- }
- });
+ when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(new SensorData());
mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
mWakelockController, mDisplayDeviceConfig, mTestLooper.getLooper(),
mNudgeUpdatePowerState, Display.DEFAULT_DISPLAY,
@@ -188,13 +176,7 @@
@Test
public void isProximitySensorAvailableReturnsTrueWhenNotAvailableAndHasDefault()
throws Exception {
- when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(
- new DisplayDeviceConfig.SensorData() {
- {
- type = null;
- name = null;
- }
- });
+ when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(new SensorData());
when(mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)).thenReturn(
TestUtils.createSensor(Sensor.TYPE_PROXIMITY, "proximity"));
mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
@@ -207,13 +189,7 @@
@Test
public void isProximitySensorAvailableReturnsFalseWhenNotAvailableHasDefaultNonDefaultDisplay()
throws Exception {
- when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(
- new DisplayDeviceConfig.SensorData() {
- {
- type = null;
- name = null;
- }
- });
+ when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(new SensorData());
when(mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)).thenReturn(
TestUtils.createSensor(Sensor.TYPE_PROXIMITY, "proximity"));
mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
@@ -240,12 +216,7 @@
public void notifyDisplayDeviceChangedReloadsTheProximitySensor() throws Exception {
DisplayDeviceConfig updatedDisplayDeviceConfig = mock(DisplayDeviceConfig.class);
when(updatedDisplayDeviceConfig.getProximitySensor()).thenReturn(
- new DisplayDeviceConfig.SensorData() {
- {
- type = Sensor.STRING_TYPE_PROXIMITY;
- name = null;
- }
- });
+ new SensorData(Sensor.STRING_TYPE_PROXIMITY, null));
Sensor newProxSensor = TestUtils.createSensor(
Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_PROXIMITY, 4.0f);
when(mSensorManager.getSensorList(eq(Sensor.TYPE_ALL)))
diff --git a/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java b/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java
index 4494b0c..6e2d954 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java
@@ -28,7 +28,7 @@
import androidx.test.filters.SmallTest;
import com.android.internal.annotations.Keep;
-import com.android.server.display.DisplayDeviceConfig.SensorData;
+import com.android.server.display.config.SensorData;
import org.junit.Before;
import org.junit.Test;
@@ -123,9 +123,7 @@
when(mSensorManager.getSensorList(Sensor.TYPE_ALL)).thenReturn(allSensors);
when(mSensorManager.getDefaultSensor(fallbackType)).thenReturn(defaultSensor);
- SensorData sensorData = new SensorData();
- sensorData.name = sensorName;
- sensorData.type = sensorType;
+ SensorData sensorData = new SensorData(sensorType, sensorName);
Sensor result = SensorUtils.findSensor(mSensorManager, sensorData, fallbackType);
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/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionWrapperTest.java
similarity index 92%
rename from services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java
rename to services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionWrapperTest.java
index 8608199..cfd0289 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionWrapperTest.java
@@ -37,11 +37,11 @@
import org.mockito.MockitoAnnotations;
/**
- * Tests for WindowMagnificationConnectionWrapper. We don't test {@code
- * WindowMagnificationConnectionWrapper#linkToDeath(IBinder.DeathRecipient)} since it's tested in
+ * Tests for MagnificationConnectionWrapper. We don't test {@code
+ * MagnificationConnectionWrapper#linkToDeath(IBinder.DeathRecipient)} since it's tested in
* {@link WindowMagnificationManagerTest}.
*/
-public class WindowMagnificationConnectionWrapperTest {
+public class MagnificationConnectionWrapperTest {
private static final int TEST_DISPLAY = Display.DEFAULT_DISPLAY;
@@ -54,14 +54,14 @@
private MagnificationAnimationCallback mAnimationCallback;
private MockWindowMagnificationConnection mMockWindowMagnificationConnection;
- private WindowMagnificationConnectionWrapper mConnectionWrapper;
+ private MagnificationConnectionWrapper mConnectionWrapper;
@Before
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
mMockWindowMagnificationConnection = new MockWindowMagnificationConnection();
mConnection = mMockWindowMagnificationConnection.getConnection();
- mConnectionWrapper = new WindowMagnificationConnectionWrapper(mConnection, mTrace);
+ mConnectionWrapper = new MagnificationConnectionWrapper(mConnection, mTrace);
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java
index 2cdfbff..13dc120 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java
@@ -57,7 +57,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.app.IBatteryStats;
-import com.android.net.flags.Flags;
+import com.android.modules.utils.build.SdkLevel;
import org.junit.After;
import org.junit.Before;
@@ -264,7 +264,7 @@
verify(mCm).addUidToMeteredNetworkDenyList(TEST_UID);
mNMService.setDataSaverModeEnabled(true);
- if (Flags.setDataSaverViaCm()) {
+ if (SdkLevel.isAtLeastV()) {
verify(mCm).setDataSaverEnabled(true);
} else {
verify(mNetdService).bandwidthEnableDataSaver(true);
@@ -284,7 +284,7 @@
mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, false);
verify(mCm).removeUidFromMeteredNetworkAllowList(TEST_UID);
mNMService.setDataSaverModeEnabled(false);
- if (Flags.setDataSaverViaCm()) {
+ if (SdkLevel.isAtLeastV()) {
verify(mCm).setDataSaverEnabled(false);
} else {
verify(mNetdService).bandwidthEnableDataSaver(false);
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
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 3803244..7fb8b30 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -3239,7 +3239,7 @@
mService.setPreferencesHelper(mPreferencesHelper);
when(mPreferencesHelper.getNotificationChannel(
anyString(), anyInt(), eq("foo"), anyBoolean())).thenReturn(
- new NotificationChannel("foo", "foo", IMPORTANCE_HIGH));
+ new NotificationChannel("foo", "foo", IMPORTANCE_HIGH));
Notification.TvExtender tv = new Notification.TvExtender().setChannelId("foo");
mBinderService.enqueueNotificationWithTag(PKG, PKG, "testTvExtenderChannelOverride_onTv", 0,
@@ -9927,6 +9927,174 @@
}
@Test
+ public void testRestoreConversationChannel_deleted() throws Exception {
+ // Create parent channel
+ when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(false);
+ final NotificationChannel originalChannel = new NotificationChannel("id", "name",
+ IMPORTANCE_DEFAULT);
+ NotificationChannel parentChannel = parcelAndUnparcel(originalChannel,
+ NotificationChannel.CREATOR);
+ assertEquals(originalChannel, parentChannel);
+ mBinderService.createNotificationChannels(PKG,
+ new ParceledListSlice(Arrays.asList(parentChannel)));
+
+ //Create deleted conversation channel
+ mBinderService.createConversationNotificationChannelForPackage(
+ PKG, mUid, parentChannel, VALID_CONVO_SHORTCUT_ID);
+ final NotificationChannel conversationChannel =
+ mBinderService.getConversationNotificationChannel(
+ PKG, mUserId, PKG, originalChannel.getId(), false, VALID_CONVO_SHORTCUT_ID);
+ conversationChannel.setDeleted(true);
+
+ //Create notification record
+ Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */,
+ null /* groupKey */, false /* isSummary */);
+ nb.setShortcutId(VALID_CONVO_SHORTCUT_ID);
+ nb.setChannelId(originalChannel.getId());
+ StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1,
+ "tag", mUid, 0, nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
+ NotificationRecord nr = new NotificationRecord(mContext, sbn, originalChannel);
+ assertThat(nr.getChannel()).isEqualTo(originalChannel);
+
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
+ nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+ waitForIdle();
+
+ // Verify that the channel was changed to the conversation channel and restored
+ assertThat(mService.getNotificationRecord(nr.getKey()).isConversation()).isTrue();
+ assertThat(mService.getNotificationRecord(nr.getKey()).getChannel()).isEqualTo(
+ conversationChannel);
+ assertThat(mService.getNotificationRecord(nr.getKey()).getChannel().isDeleted()).isFalse();
+ assertThat(mService.getNotificationRecord(nr.getKey()).getChannel().getDeletedTimeMs())
+ .isEqualTo(-1);
+ }
+
+ @Test
+ public void testDoNotRestoreParentChannel_deleted() throws Exception {
+ // Create parent channel and set as deleted
+ when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(false);
+ final NotificationChannel originalChannel = new NotificationChannel("id", "name",
+ IMPORTANCE_DEFAULT);
+ NotificationChannel parentChannel = parcelAndUnparcel(originalChannel,
+ NotificationChannel.CREATOR);
+ assertEquals(originalChannel, parentChannel);
+ mBinderService.createNotificationChannels(PKG,
+ new ParceledListSlice(Arrays.asList(parentChannel)));
+ parentChannel.setDeleted(true);
+
+ //Create notification record
+ Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */,
+ null /* groupKey */, false /* isSummary */);
+ nb.setShortcutId(VALID_CONVO_SHORTCUT_ID);
+ nb.setChannelId(originalChannel.getId());
+ StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1,
+ "tag", mUid, 0, nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
+ NotificationRecord nr = new NotificationRecord(mContext, sbn, originalChannel);
+ assertThat(nr.getChannel()).isEqualTo(originalChannel);
+
+ when(mPermissionHelper.hasPermission(mUid)).thenReturn(true);
+
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
+ nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+ waitForIdle();
+
+ // Verify that the channel was not restored and the notification was not posted
+ assertThat(mService.mChannelToastsSent).contains(mUid);
+ assertThat(mService.getNotificationRecord(nr.getKey())).isNull();
+ assertThat(parentChannel.isDeleted()).isTrue();
+ }
+
+ @Test
+ public void testEnqueueToConversationChannel_notDeleted_doesNotRestore() throws Exception {
+ TestableNotificationManagerService service = spy(mService);
+ PreferencesHelper preferencesHelper = spy(mService.mPreferencesHelper);
+ service.setPreferencesHelper(preferencesHelper);
+ // Create parent channel
+ when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(false);
+ final NotificationChannel originalChannel = new NotificationChannel("id", "name",
+ IMPORTANCE_DEFAULT);
+ NotificationChannel parentChannel = parcelAndUnparcel(originalChannel,
+ NotificationChannel.CREATOR);
+ assertEquals(originalChannel, parentChannel);
+ mBinderService.createNotificationChannels(PKG,
+ new ParceledListSlice(Arrays.asList(parentChannel)));
+
+ //Create conversation channel
+ mBinderService.createConversationNotificationChannelForPackage(
+ PKG, mUid, parentChannel, VALID_CONVO_SHORTCUT_ID);
+ final NotificationChannel conversationChannel =
+ mBinderService.getConversationNotificationChannel(
+ PKG, mUserId, PKG, originalChannel.getId(), false, VALID_CONVO_SHORTCUT_ID);
+
+ //Create notification record
+ Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */,
+ null /* groupKey */, false /* isSummary */);
+ nb.setShortcutId(VALID_CONVO_SHORTCUT_ID);
+ nb.setChannelId(originalChannel.getId());
+ StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1,
+ "tag", mUid, 0, nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
+ NotificationRecord nr = new NotificationRecord(mContext, sbn, originalChannel);
+ assertThat(nr.getChannel()).isEqualTo(originalChannel);
+
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
+ nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+ waitForIdle();
+
+ // Verify that the channel was changed to the conversation channel and not restored
+ assertThat(service.getNotificationRecord(nr.getKey()).isConversation()).isTrue();
+ assertThat(service.getNotificationRecord(nr.getKey()).getChannel()).isEqualTo(
+ conversationChannel);
+ verify(service, never()).handleSavePolicyFile();
+ verify(preferencesHelper, never()).createNotificationChannel(anyString(),
+ anyInt(), any(), anyBoolean(), anyBoolean(), anyInt(), anyBoolean());
+ }
+
+ @Test
+ public void testEnqueueToParentChannel_notDeleted_doesNotRestore() throws Exception {
+ TestableNotificationManagerService service = spy(mService);
+ PreferencesHelper preferencesHelper = spy(mService.mPreferencesHelper);
+ service.setPreferencesHelper(preferencesHelper);
+ // Create parent channel
+ when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(false);
+ final NotificationChannel originalChannel = new NotificationChannel("id", "name",
+ IMPORTANCE_DEFAULT);
+ NotificationChannel parentChannel = parcelAndUnparcel(originalChannel,
+ NotificationChannel.CREATOR);
+ assertEquals(originalChannel, parentChannel);
+ mBinderService.createNotificationChannels(PKG,
+ new ParceledListSlice(Arrays.asList(parentChannel)));
+
+ //Create deleted conversation channel
+ mBinderService.createConversationNotificationChannelForPackage(
+ PKG, mUid, parentChannel, VALID_CONVO_SHORTCUT_ID);
+ final NotificationChannel conversationChannel =
+ mBinderService.getConversationNotificationChannel(
+ PKG, mUserId, PKG, originalChannel.getId(), false, VALID_CONVO_SHORTCUT_ID);
+
+ //Create notification record without a shortcutId
+ Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */,
+ null /* groupKey */, false /* isSummary */);
+ nb.setShortcutId(null);
+ nb.setChannelId(originalChannel.getId());
+ StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1,
+ "tag", mUid, 0, nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
+ NotificationRecord nr = new NotificationRecord(mContext, sbn, originalChannel);
+ assertThat(nr.getChannel()).isEqualTo(originalChannel);
+
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
+ nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+ waitForIdle();
+
+ // Verify that the channel is the parent channel and no channel was restored
+ //assertThat(service.getNotificationRecord(nr.getKey()).isConversation()).isFalse();
+ assertThat(service.getNotificationRecord(nr.getKey()).getChannel()).isEqualTo(
+ parentChannel);
+ verify(service, never()).handleSavePolicyFile();
+ verify(preferencesHelper, never()).createNotificationChannel(anyString(),
+ anyInt(), any(), anyBoolean(), anyBoolean(), anyInt(), anyBoolean());
+ }
+
+ @Test
public void testGetConversationsForPackage_hasShortcut() throws Exception {
mService.setPreferencesHelper(mPreferencesHelper);
ArrayList<ConversationChannelWrapper> convos = new ArrayList<>();
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index 6790dc2..afea811 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -117,6 +117,20 @@
}
@Test
+ public void noBackWhenMoveTaskToBack() {
+ Task taskA = createTask(mDefaultDisplay);
+ ActivityRecord recordA = createActivityRecord(taskA);
+ Mockito.doNothing().when(recordA).reparentSurfaceControl(any(), any());
+
+ final Task topTask = createTopTaskWithActivity();
+ withSystemCallback(topTask);
+ // simulate moveTaskToBack
+ topTask.setVisibleRequested(false);
+ BackNavigationInfo backNavigationInfo = startBackNavigation();
+ assertWithMessage("BackNavigationInfo").that(backNavigationInfo).isNull();
+ }
+
+ @Test
public void backTypeCrossTaskWhenBackToPreviousTask() {
Task taskA = createTask(mDefaultDisplay);
ActivityRecord recordA = createActivityRecord(taskA);
diff --git a/telephony/java/android/telephony/PreciseDataConnectionState.java b/telephony/java/android/telephony/PreciseDataConnectionState.java
index 7f1c14b..b568f07 100644
--- a/telephony/java/android/telephony/PreciseDataConnectionState.java
+++ b/telephony/java/android/telephony/PreciseDataConnectionState.java
@@ -16,6 +16,8 @@
package android.telephony;
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
@@ -37,8 +39,11 @@
import android.telephony.data.DataCallResponse;
import android.telephony.data.Qos;
+import com.android.internal.telephony.flags.Flags;
import com.android.internal.telephony.util.TelephonyUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
@@ -66,6 +71,53 @@
private final LinkProperties mLinkProperties;
private final ApnSetting mApnSetting;
private final Qos mDefaultQos;
+ private final @NetworkValidationStatus int mNetworkValidationStatus;
+
+ /** @hide */
+ @IntDef(prefix = "NETWORK_VALIDATION_", value = {
+ NETWORK_VALIDATION_UNSUPPORTED,
+ NETWORK_VALIDATION_NOT_REQUESTED,
+ NETWORK_VALIDATION_IN_PROGRESS,
+ NETWORK_VALIDATION_SUCCESS,
+ NETWORK_VALIDATION_FAILURE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface NetworkValidationStatus {}
+
+ /**
+ * Unsupported. The unsupported state is used when the data network cannot support the network
+ * validation function for the current data connection state.
+ */
+ @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+ public static final int NETWORK_VALIDATION_UNSUPPORTED = 0;
+
+ /**
+ * Not Requested. The not requested status is used when the data network supports the network
+ * validation function, but no network validation is being performed yet.
+ */
+ @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+ public static final int NETWORK_VALIDATION_NOT_REQUESTED = 1;
+
+ /**
+ * In progress. The in progress state is used when the network validation process for the data
+ * network is in progress. This state is followed by either success or failure.
+ */
+ @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+ public static final int NETWORK_VALIDATION_IN_PROGRESS = 2;
+
+ /**
+ * Success. The Success status is used when network validation has been completed for the data
+ * network and the result is successful.
+ */
+ @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+ public static final int NETWORK_VALIDATION_SUCCESS = 3;
+
+ /**
+ * Failure. The Failure status is used when network validation has been completed for the data
+ * network and the result is failure.
+ */
+ @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+ public static final int NETWORK_VALIDATION_FAILURE = 4;
/**
* Constructor
@@ -87,7 +139,7 @@
.setApnTypeBitmask(apnTypes)
.setApnName(apn)
.setEntryName(apn)
- .build(), null);
+ .build(), null, NETWORK_VALIDATION_UNSUPPORTED);
}
@@ -109,7 +161,8 @@
private PreciseDataConnectionState(@TransportType int transportType, int id,
@DataState int state, @NetworkType int networkType,
@Nullable LinkProperties linkProperties, @DataFailureCause int failCause,
- @Nullable ApnSetting apnSetting, @Nullable Qos defaultQos) {
+ @Nullable ApnSetting apnSetting, @Nullable Qos defaultQos,
+ @NetworkValidationStatus int networkValidationStatus) {
mTransportType = transportType;
mId = id;
mState = state;
@@ -118,6 +171,7 @@
mFailCause = failCause;
mApnSetting = apnSetting;
mDefaultQos = defaultQos;
+ mNetworkValidationStatus = networkValidationStatus;
}
/**
@@ -140,6 +194,7 @@
mDefaultQos = in.readParcelable(
Qos.class.getClassLoader(),
android.telephony.data.Qos.class);
+ mNetworkValidationStatus = in.readInt();
}
/**
@@ -289,6 +344,16 @@
return mDefaultQos;
}
+ /**
+ * Returns the network validation state.
+ *
+ * @return the network validation status of the data call
+ */
+ @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+ public @NetworkValidationStatus int getNetworkValidationStatus() {
+ return mNetworkValidationStatus;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -304,6 +369,7 @@
out.writeInt(mFailCause);
out.writeParcelable(mApnSetting, flags);
out.writeParcelable(mDefaultQos, flags);
+ out.writeInt(mNetworkValidationStatus);
}
public static final @NonNull Parcelable.Creator<PreciseDataConnectionState> CREATOR
@@ -321,7 +387,7 @@
@Override
public int hashCode() {
return Objects.hash(mTransportType, mId, mState, mNetworkType, mFailCause,
- mLinkProperties, mApnSetting, mDefaultQos);
+ mLinkProperties, mApnSetting, mDefaultQos, mNetworkValidationStatus);
}
@@ -337,7 +403,8 @@
&& mFailCause == that.mFailCause
&& Objects.equals(mLinkProperties, that.mLinkProperties)
&& Objects.equals(mApnSetting, that.mApnSetting)
- && Objects.equals(mDefaultQos, that.mDefaultQos);
+ && Objects.equals(mDefaultQos, that.mDefaultQos)
+ && mNetworkValidationStatus == that.mNetworkValidationStatus;
}
@NonNull
@@ -354,11 +421,34 @@
sb.append(", link properties: " + mLinkProperties);
sb.append(", default QoS: " + mDefaultQos);
sb.append(", fail cause: " + DataFailCause.toString(mFailCause));
+ sb.append(", network validation status: "
+ + networkValidationStatusToString(mNetworkValidationStatus));
return sb.toString();
}
/**
+ * Convert a network validation status to string.
+ *
+ * @param networkValidationStatus network validation status.
+ * @return string of validation status.
+ *
+ * @hide
+ */
+ @NonNull
+ public static String networkValidationStatusToString(
+ @NetworkValidationStatus int networkValidationStatus) {
+ switch (networkValidationStatus) {
+ case NETWORK_VALIDATION_UNSUPPORTED: return "unsupported";
+ case NETWORK_VALIDATION_NOT_REQUESTED: return "not requested";
+ case NETWORK_VALIDATION_IN_PROGRESS: return "in progress";
+ case NETWORK_VALIDATION_SUCCESS: return "success";
+ case NETWORK_VALIDATION_FAILURE: return "failure";
+ default: return Integer.toString(networkValidationStatus);
+ }
+ }
+
+ /**
* {@link PreciseDataConnectionState} builder
*
* @hide
@@ -394,6 +484,10 @@
/** The Default QoS for this EPS/5GS bearer or null otherwise */
private @Nullable Qos mDefaultQos;
+ /** The network validation status for the data connection. */
+ private @NetworkValidationStatus int mNetworkValidationStatus =
+ NETWORK_VALIDATION_UNSUPPORTED;
+
/**
* Set the transport type of the data connection.
*
@@ -486,13 +580,27 @@
}
/**
+ * Set the network validation state for the data connection.
+ *
+ * @param networkValidationStatus the network validation status of the data call
+ * @return The builder
+ */
+ @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+ public @NonNull Builder setNetworkValidationStatus(
+ @NetworkValidationStatus int networkValidationStatus) {
+ mNetworkValidationStatus = networkValidationStatus;
+ return this;
+ }
+
+ /**
* Build the {@link PreciseDataConnectionState} instance.
*
* @return The {@link PreciseDataConnectionState} instance
*/
public PreciseDataConnectionState build() {
return new PreciseDataConnectionState(mTransportType, mId, mState, mNetworkType,
- mLinkProperties, mFailCause, mApnSetting, mDefaultQos);
+ mLinkProperties, mFailCause, mApnSetting, mDefaultQos,
+ mNetworkValidationStatus);
}
}
}
diff --git a/telephony/java/android/telephony/data/DataCallResponse.java b/telephony/java/android/telephony/data/DataCallResponse.java
index c7f0c5f..9dd83d1 100644
--- a/telephony/java/android/telephony/data/DataCallResponse.java
+++ b/telephony/java/android/telephony/data/DataCallResponse.java
@@ -17,6 +17,7 @@
package android.telephony.data;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -27,9 +28,11 @@
import android.os.Parcelable;
import android.telephony.Annotation.DataFailureCause;
import android.telephony.DataFailCause;
+import android.telephony.PreciseDataConnectionState;
import android.telephony.data.ApnSetting.ProtocolType;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.flags.Flags;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
@@ -123,7 +126,6 @@
* Indicates that the pdu session id is not set.
*/
public static final int PDU_SESSION_ID_NOT_SET = 0;
-
private final @DataFailureCause int mCause;
private final long mSuggestedRetryTime;
private final int mId;
@@ -143,6 +145,7 @@
private final List<QosBearerSession> mQosBearerSessions;
private final NetworkSliceInfo mSliceInfo;
private final List<TrafficDescriptor> mTrafficDescriptors;
+ private final @PreciseDataConnectionState.NetworkValidationStatus int mNetworkValidationStatus;
/**
* @param cause Data call fail cause. {@link DataFailCause#NONE} indicates no error.
@@ -185,7 +188,8 @@
HANDOVER_FAILURE_MODE_LEGACY, PDU_SESSION_ID_NOT_SET,
null /* defaultQos */, Collections.emptyList() /* qosBearerSessions */,
null /* sliceInfo */,
- Collections.emptyList() /* trafficDescriptors */);
+ Collections.emptyList(), /* trafficDescriptors */
+ PreciseDataConnectionState.NETWORK_VALIDATION_UNSUPPORTED);
}
private DataCallResponse(@DataFailureCause int cause, long suggestedRetryTime, int id,
@@ -196,7 +200,8 @@
@HandoverFailureMode int handoverFailureMode, int pduSessionId,
@Nullable Qos defaultQos, @NonNull List<QosBearerSession> qosBearerSessions,
@Nullable NetworkSliceInfo sliceInfo,
- @NonNull List<TrafficDescriptor> trafficDescriptors) {
+ @NonNull List<TrafficDescriptor> trafficDescriptors,
+ @PreciseDataConnectionState.NetworkValidationStatus int networkValidationStatus) {
mCause = cause;
mSuggestedRetryTime = suggestedRetryTime;
mId = id;
@@ -216,6 +221,7 @@
mQosBearerSessions = new ArrayList<>(qosBearerSessions);
mSliceInfo = sliceInfo;
mTrafficDescriptors = new ArrayList<>(trafficDescriptors);
+ mNetworkValidationStatus = networkValidationStatus;
if (mLinkStatus == LINK_STATUS_ACTIVE
|| mLinkStatus == LINK_STATUS_DORMANT) {
@@ -270,6 +276,7 @@
source.readList(mTrafficDescriptors,
TrafficDescriptor.class.getClassLoader(),
android.telephony.data.TrafficDescriptor.class);
+ mNetworkValidationStatus = source.readInt();
}
/**
@@ -442,6 +449,17 @@
return Collections.unmodifiableList(mTrafficDescriptors);
}
+ /**
+ * Return the network validation status that was initiated by {@link
+ * DataService.DataServiceProvider#requestValidation}
+ *
+ * @return The network validation status of data connection.
+ */
+ @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+ public @PreciseDataConnectionState.NetworkValidationStatus int getNetworkValidationStatus() {
+ return mNetworkValidationStatus;
+ }
+
@NonNull
@Override
public String toString() {
@@ -466,6 +484,8 @@
.append(" qosBearerSessions=").append(mQosBearerSessions)
.append(" sliceInfo=").append(mSliceInfo)
.append(" trafficDescriptors=").append(mTrafficDescriptors)
+ .append(" networkValidationStatus=").append(PreciseDataConnectionState
+ .networkValidationStatusToString(mNetworkValidationStatus))
.append("}");
return sb.toString();
}
@@ -504,7 +524,8 @@
&& mQosBearerSessions.containsAll(other.mQosBearerSessions) // non-null
&& Objects.equals(mSliceInfo, other.mSliceInfo)
&& mTrafficDescriptors.size() == other.mTrafficDescriptors.size() // non-null
- && mTrafficDescriptors.containsAll(other.mTrafficDescriptors); // non-null
+ && mTrafficDescriptors.containsAll(other.mTrafficDescriptors) // non-null
+ && mNetworkValidationStatus == other.mNetworkValidationStatus;
}
@Override
@@ -513,7 +534,7 @@
mInterfaceName, Set.copyOf(mAddresses), Set.copyOf(mDnsAddresses),
Set.copyOf(mGatewayAddresses), Set.copyOf(mPcscfAddresses), mMtu, mMtuV4, mMtuV6,
mHandoverFailureMode, mPduSessionId, mDefaultQos, Set.copyOf(mQosBearerSessions),
- mSliceInfo, Set.copyOf(mTrafficDescriptors));
+ mSliceInfo, Set.copyOf(mTrafficDescriptors), mNetworkValidationStatus);
}
@Override
@@ -542,6 +563,7 @@
dest.writeList(mQosBearerSessions);
dest.writeParcelable(mSliceInfo, flags);
dest.writeList(mTrafficDescriptors);
+ dest.writeInt(mNetworkValidationStatus);
}
public static final @android.annotation.NonNull Parcelable.Creator<DataCallResponse> CREATOR =
@@ -629,6 +651,9 @@
private List<TrafficDescriptor> mTrafficDescriptors = new ArrayList<>();
+ private @PreciseDataConnectionState.NetworkValidationStatus int mNetworkValidationStatus =
+ PreciseDataConnectionState.NETWORK_VALIDATION_UNSUPPORTED;
+
/**
* Default constructor for Builder.
*/
@@ -905,6 +930,20 @@
}
/**
+ * Set the network validation status that corresponds to the state of the network validation
+ * request started by {@link DataService.DataServiceProvider#requestValidation}
+ *
+ * @param status The network validation status.
+ * @return The same instance of the builder.
+ */
+ @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+ public @NonNull Builder setNetworkValidationStatus(
+ @PreciseDataConnectionState.NetworkValidationStatus int status) {
+ mNetworkValidationStatus = status;
+ return this;
+ }
+
+ /**
* Build the DataCallResponse.
*
* @return the DataCallResponse object.
@@ -913,7 +952,8 @@
return new DataCallResponse(mCause, mSuggestedRetryTime, mId, mLinkStatus,
mProtocolType, mInterfaceName, mAddresses, mDnsAddresses, mGatewayAddresses,
mPcscfAddresses, mMtu, mMtuV4, mMtuV6, mHandoverFailureMode, mPduSessionId,
- mDefaultQos, mQosBearerSessions, mSliceInfo, mTrafficDescriptors);
+ mDefaultQos, mQosBearerSessions, mSliceInfo, mTrafficDescriptors,
+ mNetworkValidationStatus);
}
}
}
diff --git a/telephony/java/android/telephony/data/DataService.java b/telephony/java/android/telephony/data/DataService.java
index d8b2cbe..80e91a3 100644
--- a/telephony/java/android/telephony/data/DataService.java
+++ b/telephony/java/android/telephony/data/DataService.java
@@ -16,6 +16,8 @@
package android.telephony.data;
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -26,6 +28,7 @@
import android.content.Intent;
import android.net.LinkProperties;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
@@ -36,6 +39,9 @@
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.IIntegerConsumer;
+import com.android.internal.telephony.flags.Flags;
+import com.android.internal.util.FunctionalUtils;
import com.android.telephony.Rlog;
import java.lang.annotation.Retention;
@@ -44,6 +50,8 @@
import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* Base class of data service. Services that extend DataService must register the service in
@@ -113,11 +121,14 @@
private static final int DATA_SERVICE_REQUEST_REGISTER_APN_UNTHROTTLED = 14;
private static final int DATA_SERVICE_REQUEST_UNREGISTER_APN_UNTHROTTLED = 15;
private static final int DATA_SERVICE_INDICATION_APN_UNTHROTTLED = 16;
+ private static final int DATA_SERVICE_REQUEST_VALIDATION = 17;
private final HandlerThread mHandlerThread;
private final DataServiceHandler mHandler;
+ private final Executor mHandlerExecutor;
+
private final SparseArray<DataServiceProvider> mServiceMap = new SparseArray<>();
/** @hide */
@@ -379,6 +390,43 @@
}
}
+ /**
+ * Request validation check to see if the network is working properly for a given data call.
+ *
+ * <p>This request is completed immediately after submitting the request to the data service
+ * provider and receiving {@link DataServiceCallback.ResultCode}, and progress status or
+ * validation results are notified through {@link
+ * DataCallResponse#getNetworkValidationStatus}.
+ *
+ * <p> If the network validation request is submitted successfully, {@link
+ * DataServiceCallback#RESULT_SUCCESS} is passed to {@code resultCodeCallback}. If the
+ * network validation feature is not supported by the data service provider itself, {@link
+ * DataServiceCallback#RESULT_ERROR_UNSUPPORTED} is passed to {@code resultCodeCallback}.
+ * See {@link DataServiceCallback.ResultCode} for the type of response that indicates
+ * whether the request was successfully submitted or had an error.
+ *
+ * <p>In response to this network validation request, providers can validate the data call
+ * in their own way. For example, in IWLAN, the DPD (Dead Peer Detection) can be used as a
+ * tool to check whether a data call is alive.
+ *
+ * @param cid The identifier of the data call which is provided in {@link DataCallResponse}
+ * @param executor The callback executor for the response.
+ * @param resultCodeCallback Listener for the {@link DataServiceCallback.ResultCode} that
+ * request validation to the DataService and checks if the request has been submitted.
+ */
+ @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+ public void requestValidation(int cid,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull @DataServiceCallback.ResultCode Consumer<Integer> resultCodeCallback) {
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(resultCodeCallback, "resultCodeCallback cannot be null");
+
+ Log.d(TAG, "requestValidation: " + cid);
+
+ // The default implementation is to return unsupported.
+ executor.execute(() -> resultCodeCallback
+ .accept(DataServiceCallback.RESULT_ERROR_UNSUPPORTED));
+ }
/**
* Notify the system that current data call list changed. Data service must invoke this
@@ -537,6 +585,17 @@
}
}
+ private static final class ValidationRequest {
+ public final int cid;
+ public final Executor executor;
+ public final IIntegerConsumer callback;
+ ValidationRequest(int cid, Executor executor, IIntegerConsumer callback) {
+ this.cid = cid;
+ this.executor = executor;
+ this.callback = callback;
+ }
+ }
+
private class DataServiceHandler extends Handler {
DataServiceHandler(Looper looper) {
@@ -679,6 +738,15 @@
loge("Failed to call onApnUnthrottled. " + e);
}
break;
+ case DATA_SERVICE_REQUEST_VALIDATION:
+ if (serviceProvider == null) break;
+ ValidationRequest validationRequest = (ValidationRequest) message.obj;
+ serviceProvider.requestValidation(
+ validationRequest.cid,
+ validationRequest.executor,
+ FunctionalUtils
+ .ignoreRemoteException(validationRequest.callback::accept));
+ break;
}
}
}
@@ -691,6 +759,7 @@
mHandlerThread.start();
mHandler = new DataServiceHandler(mHandlerThread.getLooper());
+ mHandlerExecutor = new HandlerExecutor(mHandler);
log("Data service created");
}
@@ -853,6 +922,18 @@
mHandler.obtainMessage(DATA_SERVICE_REQUEST_UNREGISTER_APN_UNTHROTTLED,
slotIndex, 0, callback).sendToTarget();
}
+
+ @Override
+ public void requestValidation(int slotIndex, int cid, IIntegerConsumer resultCodeCallback) {
+ if (resultCodeCallback == null) {
+ loge("requestValidation: resultCodeCallback is null");
+ return;
+ }
+ ValidationRequest validationRequest =
+ new ValidationRequest(cid, mHandlerExecutor, resultCodeCallback);
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_VALIDATION,
+ slotIndex, 0, validationRequest).sendToTarget();
+ }
}
private void log(String s) {
diff --git a/telephony/java/android/telephony/data/IDataService.aidl b/telephony/java/android/telephony/data/IDataService.aidl
index 1346946..15f8881 100644
--- a/telephony/java/android/telephony/data/IDataService.aidl
+++ b/telephony/java/android/telephony/data/IDataService.aidl
@@ -22,6 +22,8 @@
import android.telephony.data.NetworkSliceInfo;
import android.telephony.data.TrafficDescriptor;
+import com.android.internal.telephony.IIntegerConsumer;
+
/**
* {@hide}
*/
@@ -46,4 +48,5 @@
void cancelHandover(int slotId, int cid, IDataServiceCallback callback);
void registerForUnthrottleApn(int slotIndex, IDataServiceCallback callback);
void unregisterForUnthrottleApn(int slotIndex, IDataServiceCallback callback);
+ void requestValidation(int slotId, int cid, IIntegerConsumer callback);
}
diff --git a/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl b/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl
index 32ffdbc..bdd212a 100644
--- a/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl
+++ b/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl
@@ -16,6 +16,8 @@
package android.telephony.data;
+import com.android.internal.telephony.IIntegerConsumer;
+
/**
* The qualified networks service call back interface
* @hide
@@ -23,4 +25,5 @@
oneway interface IQualifiedNetworksServiceCallback
{
void onQualifiedNetworkTypesChanged(int apnTypes, in int[] qualifiedNetworkTypes);
+ void onNetworkValidationRequested(int networkCapability, IIntegerConsumer callback);
}
diff --git a/telephony/java/android/telephony/data/QualifiedNetworksService.java b/telephony/java/android/telephony/data/QualifiedNetworksService.java
index 56f0f9f..c3ba092 100644
--- a/telephony/java/android/telephony/data/QualifiedNetworksService.java
+++ b/telephony/java/android/telephony/data/QualifiedNetworksService.java
@@ -16,6 +16,8 @@
package android.telephony.data;
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.app.Service;
@@ -29,13 +31,23 @@
import android.telephony.AccessNetworkConstants;
import android.telephony.AccessNetworkConstants.AccessNetworkType;
import android.telephony.Annotation.ApnType;
+import android.telephony.Annotation.NetCapability;
+import android.telephony.PreciseDataConnectionState;
import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.IIntegerConsumer;
+import com.android.internal.telephony.flags.FeatureFlags;
+import com.android.internal.telephony.flags.FeatureFlagsImpl;
+import com.android.internal.telephony.flags.Flags;
+import com.android.internal.util.FunctionalUtils;
import com.android.telephony.Rlog;
import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* Base class of the qualified networks service, which is a vendor service providing up-to-date
@@ -69,6 +81,10 @@
private static final int QNS_UPDATE_QUALIFIED_NETWORKS = 4;
private static final int QNS_APN_THROTTLE_STATUS_CHANGED = 5;
private static final int QNS_EMERGENCY_DATA_NETWORK_PREFERRED_TRANSPORT_CHANGED = 6;
+ private static final int QNS_REQUEST_NETWORK_VALIDATION = 7;
+
+ /** Feature flags */
+ private static final FeatureFlags sFeatureFlag = new FeatureFlagsImpl();
private final HandlerThread mHandlerThread;
@@ -208,6 +224,72 @@
}
/**
+ * Request network validation to the connected data network for given a network capability.
+ *
+ * <p>This network validation can only be performed when a data network is in connected
+ * state, and will not be triggered if the data network does not support network validation
+ * feature or network validation is not in connected state.
+ *
+ * <p>See {@link DataServiceCallback.ResultCode} for the type of response that indicates
+ * whether the request was successfully submitted or had an error.
+ *
+ * <p>If network validation is requested, monitor network validation status in {@link
+ * PreciseDataConnectionState#getNetworkValidationStatus()}.
+ *
+ * @param networkCapability A network capability. (Note that only APN-type capabilities are
+ * supported.
+ * @param executor executor The callback executor that responds whether the request has been
+ * successfully submitted or not.
+ * @param resultCodeCallback A callback to determine whether the request was successfully
+ * submitted or not.
+ */
+ @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+ public void requestNetworkValidation(
+ @NetCapability int networkCapability,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull @DataServiceCallback.ResultCode Consumer<Integer> resultCodeCallback) {
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(resultCodeCallback, "resultCodeCallback cannot be null");
+
+ if (!sFeatureFlag.networkValidation()) {
+ loge("networkValidation feature is disabled");
+ executor.execute(
+ () ->
+ resultCodeCallback.accept(
+ DataServiceCallback.RESULT_ERROR_UNSUPPORTED));
+ return;
+ }
+
+ IIntegerConsumer callback = new IIntegerConsumer.Stub() {
+ @Override
+ public void accept(int result) {
+ executor.execute(() -> resultCodeCallback.accept(result));
+ }
+ };
+
+ // Move to the internal handler and process it.
+ mHandler.obtainMessage(
+ QNS_REQUEST_NETWORK_VALIDATION,
+ mSlotIndex,
+ 0,
+ new NetworkValidationRequestData(networkCapability, callback))
+ .sendToTarget();
+ }
+
+ /** Process a network validation request on the internal handler. */
+ private void onRequestNetworkValidation(NetworkValidationRequestData data) {
+ try {
+ log("onRequestNetworkValidation");
+ // Callback to request a network validation.
+ mCallback.onNetworkValidationRequested(data.mNetworkCapability, data.mCallback);
+ } catch (RemoteException | NullPointerException e) {
+ loge("Failed to call onRequestNetworkValidation. " + e);
+ FunctionalUtils.ignoreRemoteException(data.mCallback::accept)
+ .accept(DataServiceCallback.RESULT_ERROR_UNSUPPORTED);
+ }
+ }
+
+ /**
* Called when the qualified networks provider is removed. The extended class should
* implement this method to perform cleanup works.
*/
@@ -280,6 +362,10 @@
if (provider == null) break;
provider.onUpdateQualifiedNetworkTypes(message.arg2, (int[]) message.obj);
break;
+
+ case QNS_REQUEST_NETWORK_VALIDATION:
+ if (provider == null) break;
+ provider.onRequestNetworkValidation((NetworkValidationRequestData) message.obj);
}
}
}
@@ -364,6 +450,17 @@
}
}
+ private static final class NetworkValidationRequestData {
+ final @NetCapability int mNetworkCapability;
+ final IIntegerConsumer mCallback;
+
+ private NetworkValidationRequestData(@NetCapability int networkCapability,
+ @NonNull IIntegerConsumer callback) {
+ mNetworkCapability = networkCapability;
+ mCallback = callback;
+ }
+ }
+
private void log(String s) {
Rlog.d(TAG, s);
}
diff --git a/tools/hoststubgen/hoststubgen/Android.bp b/tools/hoststubgen/hoststubgen/Android.bp
index fd4ec8b..5949bca 100644
--- a/tools/hoststubgen/hoststubgen/Android.bp
+++ b/tools/hoststubgen/hoststubgen/Android.bp
@@ -284,6 +284,9 @@
"hoststubgen-helper-runtime.ravenwood",
"framework-minus-apex.ravenwood",
],
+ static_libs: [
+ "core-xml-for-device",
+ ],
}
// Defaults for host side test modules.