diff options
157 files changed, 4027 insertions, 1052 deletions
diff --git a/config/preloaded-classes-denylist b/config/preloaded-classes-denylist index e3e929cb00d9..a6a1d1680b7b 100644 --- a/config/preloaded-classes-denylist +++ b/config/preloaded-classes-denylist @@ -1,5 +1,4 @@ android.content.AsyncTaskLoader$LoadTask -android.media.MediaCodecInfo$CodecCapabilities$FeatureList android.net.ConnectivityThread$Singleton android.os.FileObserver android.os.NullVibrator diff --git a/core/api/current.txt b/core/api/current.txt index 6707c15de682..b7f7a7f9e779 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -16896,6 +16896,7 @@ package android.graphics { method public android.graphics.Paint.FontMetricsInt getFontMetricsInt(); method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public void getFontMetricsIntForLocale(@NonNull android.graphics.Paint.FontMetricsInt); method public float getFontSpacing(); + method @FlaggedApi("com.android.text.flags.typeface_redesign_readonly") @Nullable public String getFontVariationOverride(); method public String getFontVariationSettings(); method public int getHinting(); method public float getLetterSpacing(); @@ -16974,6 +16975,7 @@ package android.graphics { method public void setFilterBitmap(boolean); method public void setFlags(int); method public void setFontFeatureSettings(String); + method @FlaggedApi("com.android.text.flags.typeface_redesign_readonly") public boolean setFontVariationOverride(@Nullable String); method public boolean setFontVariationSettings(String); method public void setHinting(int); method public void setLetterSpacing(float); diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java index e271cf4f60ec..4e292d0b7d18 100644 --- a/core/java/android/content/ClipData.java +++ b/core/java/android/content/ClipData.java @@ -221,6 +221,12 @@ public class ClipData implements Parcelable { // if the data is obtained from {@link #copyForTransferWithActivityInfo} private ActivityInfo mActivityInfo; + private boolean mTokenVerificationEnabled; + + void setTokenVerificationEnabled() { + mTokenVerificationEnabled = true; + } + /** * A builder for a ClipData Item. */ @@ -398,7 +404,9 @@ public class ClipData implements Parcelable { * Retrieve the raw Intent contained in this Item. */ public Intent getIntent() { - Intent.maybeMarkAsMissingCreatorToken(mIntent); + if (mTokenVerificationEnabled) { + Intent.maybeMarkAsMissingCreatorToken(mIntent); + } return mIntent; } @@ -1353,6 +1361,12 @@ public class ClipData implements Parcelable { } } + void setTokenVerificationEnabled() { + for (int i = 0; i < mItems.size(); ++i) { + mItems.get(i).setTokenVerificationEnabled(); + } + } + @Override public int describeContents() { return 0; diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 350048df3112..01e24d81a7cd 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -12506,6 +12506,9 @@ public class Intent implements Parcelable, Cloneable { if (intent.mExtras != null) { intent.mExtras.enableTokenVerification(); } + if (intent.mClipData != null) { + intent.mClipData.setTokenVerificationEnabled(); + } }; /** @hide */ @@ -12517,6 +12520,9 @@ public class Intent implements Parcelable, Cloneable { // mark trusted creator token present. mExtras.enableTokenVerification(); } + if (mClipData != null) { + mClipData.setTokenVerificationEnabled(); + } } /** @hide */ diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index c16582f19c9b..8c7e93a834b7 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -4649,6 +4649,7 @@ public abstract class PackageManager { * the Android Keystore backed by an isolated execution environment. The version indicates * which features are implemented in the isolated execution environment: * <ul> + * <li>400: Inclusion of module information (via tag MODULE_HASH) in the attestation record. * <li>300: Ability to include a second IMEI in the ID attestation record, see * {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}. * <li>200: Hardware support for Curve 25519 (including both Ed25519 signature generation and @@ -4682,6 +4683,7 @@ public abstract class PackageManager { * StrongBox</a>. If this feature has a version, the version number indicates which features are * implemented in StrongBox: * <ul> + * <li>400: Inclusion of module information (via tag MODULE_HASH) in the attestation record. * <li>300: Ability to include a second IMEI in the ID attestation record, see * {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}. * <li>200: No new features for StrongBox (the Android Keystore environment backed by an diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index e24f08b7dfe5..8b8369890d1b 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -110,6 +110,14 @@ flag { } flag { + name: "allow_thermal_hal_skin_forecast" + is_exported: true + namespace: "game" + description: "Enable thermal HAL skin temperature forecast to be used by headroom API" + bug: "383211885" +} + +flag { name: "allow_thermal_headroom_thresholds" is_exported: true namespace: "game" diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index d53b98c65f9a..3c53506990d1 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -16,7 +16,6 @@ package android.text; -import static com.android.graphics.hwui.flags.Flags.highContrastTextLuminance; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION; import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH; @@ -670,15 +669,11 @@ public abstract class Layout { // High-contrast text mode // Determine if the text is black-on-white or white-on-black, so we know what blendmode will // give the highest contrast and most realistic text color. - // This equation should match the one in libs/hwui/hwui/DrawTextFunctor.h - if (highContrastTextLuminance()) { - var lab = new double[3]; - ColorUtils.colorToLAB(color, lab); - return lab[0] < 50.0; - } else { - int channelSum = Color.red(color) + Color.green(color) + Color.blue(color); - return channelSum < (128 * 3); - } + // LINT.IfChange(hct_darken) + var lab = new double[3]; + ColorUtils.colorToLAB(color, lab); + return lab[0] < 50.0; + // LINT.ThenChange(/libs/hwui/hwui/DrawTextFunctor.h:hct_darken) } private boolean isJustificationRequired(int lineNum) { diff --git a/core/java/android/view/DragEvent.java b/core/java/android/view/DragEvent.java index b65e3ebc3871..77af312eac4a 100644 --- a/core/java/android/view/DragEvent.java +++ b/core/java/android/view/DragEvent.java @@ -157,6 +157,11 @@ public class DragEvent implements Parcelable { private float mOffsetY; /** + * The id of the display where the `mX` and `mY` of this event belongs to. + */ + private int mDisplayId; + + /** * The View#DRAG_FLAG_* flags used to start the current drag, only provided if the target window * has the {@link WindowManager.LayoutParams#PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP} flag * and is only sent with {@link #ACTION_DRAG_STARTED} and {@link #ACTION_DROP}. @@ -297,14 +302,15 @@ public class DragEvent implements Parcelable { private DragEvent() { } - private void init(int action, float x, float y, float offsetX, float offsetY, int flags, - ClipDescription description, ClipData data, SurfaceControl dragSurface, + private void init(int action, float x, float y, float offsetX, float offsetY, int displayId, + int flags, ClipDescription description, ClipData data, SurfaceControl dragSurface, IDragAndDropPermissions dragAndDropPermissions, Object localState, boolean result) { mAction = action; mX = x; mY = y; mOffsetX = offsetX; mOffsetY = offsetY; + mDisplayId = displayId; mFlags = flags; mClipDescription = description; mClipData = data; @@ -315,20 +321,20 @@ public class DragEvent implements Parcelable { } static DragEvent obtain() { - return DragEvent.obtain(0, 0f, 0f, 0f, 0f, 0, null, null, null, null, null, false); + return DragEvent.obtain(0, 0f, 0f, 0f, 0f, 0, 0, null, null, null, null, null, false); } /** @hide */ public static DragEvent obtain(int action, float x, float y, float offsetX, float offsetY, - int flags, Object localState, ClipDescription description, ClipData data, + int displayId, int flags, Object localState, ClipDescription description, ClipData data, SurfaceControl dragSurface, IDragAndDropPermissions dragAndDropPermissions, boolean result) { final DragEvent ev; synchronized (gRecyclerLock) { if (gRecyclerTop == null) { ev = new DragEvent(); - ev.init(action, x, y, offsetX, offsetY, flags, description, data, dragSurface, - dragAndDropPermissions, localState, result); + ev.init(action, x, y, offsetX, offsetY, displayId, flags, description, data, + dragSurface, dragAndDropPermissions, localState, result); return ev; } ev = gRecyclerTop; @@ -339,7 +345,7 @@ public class DragEvent implements Parcelable { ev.mRecycled = false; ev.mNext = null; - ev.init(action, x, y, offsetX, offsetY, flags, description, data, dragSurface, + ev.init(action, x, y, offsetX, offsetY, displayId, flags, description, data, dragSurface, dragAndDropPermissions, localState, result); return ev; @@ -349,8 +355,9 @@ public class DragEvent implements Parcelable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public static DragEvent obtain(DragEvent source) { return obtain(source.mAction, source.mX, source.mY, source.mOffsetX, source.mOffsetY, - source.mFlags, source.mLocalState, source.mClipDescription, source.mClipData, - source.mDragSurface, source.mDragAndDropPermissions, source.mDragResult); + source.mDisplayId, source.mFlags, source.mLocalState, source.mClipDescription, + source.mClipData, source.mDragSurface, source.mDragAndDropPermissions, + source.mDragResult); } /** @@ -398,6 +405,11 @@ public class DragEvent implements Parcelable { return mOffsetY; } + /** @hide */ + public int getDisplayId() { + return mDisplayId; + } + /** * Returns the {@link android.content.ClipData} object sent to the system as part of the call * to diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java index 2cc05b0bc4b0..1c36eaf99afa 100644 --- a/core/java/android/view/InputEventReceiver.java +++ b/core/java/android/view/InputEventReceiver.java @@ -177,7 +177,7 @@ public abstract class InputEventReceiver { * drag * if true, the window associated with this input channel has just lost drag */ - public void onDragEvent(boolean isExiting, float x, float y) { + public void onDragEvent(boolean isExiting, float x, float y, int displayId) { } /** diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 8ef0b0eebb8c..36671b901a6b 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -10561,13 +10561,13 @@ public final class ViewRootImpl implements ViewParent, } @Override - public void onDragEvent(boolean isExiting, float x, float y) { + public void onDragEvent(boolean isExiting, float x, float y, int displayId) { // force DRAG_EXITED_EVENT if appropriate DragEvent event = DragEvent.obtain( - isExiting ? DragEvent.ACTION_DRAG_EXITED : DragEvent.ACTION_DRAG_LOCATION, - x, y, 0 /* offsetX */, 0 /* offsetY */, 0 /* flags */, null/* localState */, - null/* description */, null /* data */, null /* dragSurface */, - null /* dragAndDropPermissions */, false /* result */); + isExiting ? DragEvent.ACTION_DRAG_EXITED : DragEvent.ACTION_DRAG_LOCATION, x, y, + 0 /* offsetX */, 0 /* offsetY */, displayId, 0 /* flags */, + null/* localState */, null/* description */, null /* data */, + null /* dragSurface */, null /* dragAndDropPermissions */, false /* result */); dispatchDragEvent(event); } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 544f42b9acfa..64277b14098d 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -157,6 +157,9 @@ public final class AccessibilityManager { /** @hide */ public static final int AUTOCLICK_CURSOR_AREA_SIZE_MAX = 100; + /** @hide */ + public static final int AUTOCLICK_CURSOR_AREA_INCREMENT_SIZE = 20; + /** * Activity action: Launch UI to manage which accessibility service or feature is assigned * to the navigation bar Accessibility button. diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index b38feeef290b..f178b0ed2043 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -167,3 +167,14 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "windowing_sdk" + name: "use_self_sync_transaction_for_layer" + description: "Always use this.getSyncTransaction for assignLayer" + bug: "388127825" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/jni/android_view_InputEventReceiver.cpp b/core/jni/android_view_InputEventReceiver.cpp index 3a1e8835c8db..6272fb1947c1 100644 --- a/core/jni/android_view_InputEventReceiver.cpp +++ b/core/jni/android_view_InputEventReceiver.cpp @@ -441,7 +441,8 @@ status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env, } env->CallVoidMethod(receiverObj.get(), gInputEventReceiverClassInfo.onDragEvent, jboolean(dragEvent->isExiting()), dragEvent->getX(), - dragEvent->getY()); + dragEvent->getY(), + static_cast<jint>(dragEvent->getDisplayId().val())); finishInputEvent(seq, /*handled=*/true); continue; } @@ -643,7 +644,7 @@ int register_android_view_InputEventReceiver(JNIEnv* env) { GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onPointerCaptureEvent", "(Z)V"); gInputEventReceiverClassInfo.onDragEvent = - GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onDragEvent", "(ZFF)V"); + GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onDragEvent", "(ZFFI)V"); gInputEventReceiverClassInfo.onTouchModeChanged = GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onTouchModeChanged", "(Z)V"); gInputEventReceiverClassInfo.onBatchedInputEventPending = diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index debc5e9a0dce..fa4c21de682e 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -6646,6 +6646,8 @@ ul.</string> <string name="satellite_notification_title">Auto connected to satellite</string> <!-- Notification summary when satellite service is auto connected. [CHAR LIMIT=NONE] --> <string name="satellite_notification_summary">You can send and receive messages without a mobile or Wi-Fi network</string> + <!-- Notification summary when satellite service connected with data service supported. [CHAR LIMIT=NONE] --> + <string name="satellite_notification_summary_with_data">You can send and receive messages and use limited data by satellite</string> <!-- Notification title when satellite service can be manually enabled. --> <string name="satellite_notification_manual_title">Use satellite messaging?</string> <!-- Notification summary when satellite service can be manually enabled. [CHAR LIMIT=NONE] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 5f6619d4e4cc..68008e57094d 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5652,6 +5652,7 @@ <!-- System notification for satellite service --> <java-symbol type="string" name="satellite_notification_title" /> <java-symbol type="string" name="satellite_notification_summary" /> + <java-symbol type="string" name="satellite_notification_summary_with_data" /> <java-symbol type="string" name="satellite_notification_manual_title" /> <java-symbol type="string" name="satellite_notification_manual_summary" /> <java-symbol type="string" name="satellite_notification_open_message" /> diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml index 06cd44e6544d..9b3a6cba5f23 100644 --- a/core/res/res/xml/sms_short_codes.xml +++ b/core/res/res/xml/sms_short_codes.xml @@ -111,7 +111,7 @@ <shortcode country="cz" premium="90\\d{5}|90\\d{3}" free="116\\d{3}" /> <!-- Germany: 4-5 digits plus 1232xxx (premium codes from http://www.vodafone.de/infofaxe/537.pdf and http://premiumdienste.eplus.de/pdf/kodex.pdf), plus EU. To keep the premium regex from being too large, it only includes payment processors that have been used by SMS malware, with the regular pattern matching the other premium short codes. --> - <shortcode country="de" pattern="\\d{4,5}|1232\\d{3}" premium="11(?:111|833)|1232(?:013|021|060|075|286|358)|118(?:44|80|86)|20[25]00|220(?:21|22|88|99)|221(?:14|21)|223(?:44|53|77)|224[13]0|225(?:20|59|90)|226(?:06|10|20|26|30|40|56|70)|227(?:07|33|39|66|76|78|79|88|99)|228(?:08|11|66|77)|23300|30030|3[12347]000|330(?:33|55|66)|33(?:233|331|366|533)|34(?:34|567)|37000|40(?:040|123|444|[3568]00)|41(?:010|414)|44(?:000|044|344|44[24]|544)|50005|50100|50123|50555|51000|52(?:255|783)|54(?:100|2542)|55(?:077|[24]00|222|333|55|[12369]55)|56(?:789|886)|60800|6[13]000|66(?:[12348]66|566|766|777|88|999)|68888|70(?:07|123|777)|76766|77(?:007|070|222|444|[567]77)|80(?:008|123|888)|82(?:002|[378]00|323|444|472|474|488|727)|83(?:005|[169]00|333|830)|84(?:141|300|32[34]|343|488|499|777|888)|85888|86(?:188|566|640|644|650|677|868|888)|870[24]9|871(?:23|[49]9)|872(?:1[0-8]|49|99)|87499|875(?:49|55|99)|876(?:0[1367]|1[1245678]|54|99)|877(?:00|99)|878(?:15|25|3[567]|8[12])|87999|880(?:08|44|55|77|99)|88688|888(?:03|10|8|89)|8899|90(?:009|999)|99999" free="116\\d{3}|81214|81215|47529|70296|83782|3011|73240|72438" /> + <shortcode country="de" pattern="\\d{4,5}|1232\\d{3}" premium="11(?:111|833)|1232(?:013|021|060|075|286|358)|118(?:44|80|86)|20[25]00|220(?:21|22|88|99)|221(?:14|21)|223(?:44|53|77)|224[13]0|225(?:20|59|90)|226(?:06|10|20|26|30|40|56|70)|227(?:07|33|39|66|76|78|79|88|99)|228(?:08|11|66|77)|23300|30030|3[12347]000|330(?:33|55|66)|33(?:233|331|366|533)|34(?:34|567)|37000|40(?:040|123|444|[3568]00)|41(?:010|414)|44(?:000|044|344|44[24]|544)|50005|50100|50123|50555|51000|52(?:255|783)|54(?:100|2542)|55(?:077|[24]00|222|333|55|[12369]55)|56(?:789|886)|60800|6[13]000|66(?:[12348]66|566|766|777|88|999)|68888|70(?:07|123|777)|76766|77(?:007|070|222|444|[567]77)|80(?:008|123|888)|82(?:002|[378]00|323|444|472|474|488|727)|83(?:005|[169]00|333|830)|84(?:141|300|32[34]|343|488|499|777|888)|85888|86(?:188|566|640|644|650|677|868|888)|870[24]9|871(?:23|[49]9)|872(?:1[0-8]|49|99)|87499|875(?:49|55|99)|876(?:0[1367]|1[1245678]|54|99)|877(?:00|99)|878(?:15|25|3[567]|8[12])|87999|880(?:08|44|55|77|99)|88688|888(?:03|10|8|89)|8899|90(?:009|999)|99999" free="116\\d{3}|81214|81215|47529|70296|83782|3011|73240|72438|70997" /> <!-- Denmark: see http://iprs.webspacecommerce.com/Denmark-Premium-Rate-Numbers --> <shortcode country="dk" pattern="\\d{4,5}" premium="1\\d{3}" free="116\\d{3}|4665" /> diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 3378cc11d565..b332cf0d751f 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -19,6 +19,7 @@ package android.graphics; import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION; +import static com.android.text.flags.Flags.FLAG_TYPEFACE_REDESIGN_READONLY; import static com.android.text.flags.Flags.FLAG_VERTICAL_TEXT_LAYOUT; import android.annotation.ColorInt; @@ -57,6 +58,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Objects; @@ -96,6 +98,7 @@ public class Paint { private LocaleList mLocales; private String mFontFeatureSettings; private String mFontVariationSettings; + private String mFontVariationOverride; private float mShadowLayerRadius; private float mShadowLayerDx; @@ -2140,6 +2143,12 @@ public class Paint { * @see #getFontVariationSettings() * @see FontVariationAxis */ + // Add following API description once the setFontVariationOverride becomes public. + // This method generates new variation instance of the {@link Typeface} instance and set it to + // this object. Therefore, subsequent {@link #setTypeface(Typeface)} call will clear the font + // variation settings. Also, creating variation instance of the Typeface requires non trivial + // amount of time and memories, therefore consider using + // {@link #setFontVariationOverride(String, int)} for better performance. public boolean setFontVariationSettings(String fontVariationSettings) { final String settings = TextUtils.nullIfEmpty(fontVariationSettings); if (settings == mFontVariationSettings @@ -2174,6 +2183,68 @@ public class Paint { } /** + * Sets TrueType or OpenType font variation settings for overriding. + * + * The settings string is constructed from multiple pairs of axis tag and style values. The axis + * tag must contain four ASCII characters and must be wrapped with single quotes (U+0027) or + * double quotes (U+0022). Axis strings that are longer or shorter than four characters, or + * contain characters outside of U+0020..U+007E are invalid. + * + * If invalid font variation settings is provided, this method does nothing and returning false + * with printing error message to the logcat. + * + * Different from {@link #setFontVariationSettings(String)}, this overrides the font variation + * settings which is already assigned to the font instance. For example, if the underlying font + * is configured as {@code 'wght' 500, 'ital' 1}, and if the override is specified as + * {@code 'wght' 700, `wdth` 150}, then the effective font variation setting is + * {@code `wght' 700, 'ital' 1, 'wdth' 150}. The `wght` value is updated by override, 'ital' + * value is preserved because no overrides, and `wdth` value is added by override. + * + * @param fontVariationOverride font variation settings. You can pass null or empty string as + * no variation settings. + * + * @return true if the provided font variation settings is valid. Otherwise returns false. + * + * @see #getFontVariationSettings() + * @see #setFontVariationSettings(String) + * @see #getFontVariationOverride() + * @see FontVariationAxis + */ + @FlaggedApi(FLAG_TYPEFACE_REDESIGN_READONLY) + public boolean setFontVariationOverride(@Nullable String fontVariationOverride) { + if (Objects.equals(fontVariationOverride, mFontVariationOverride)) { + return true; + } + + List<FontVariationAxis> axes; + try { + axes = FontVariationAxis.fromFontVariationSettingsForList(fontVariationOverride); + } catch (IllegalArgumentException e) { + Log.i(TAG, "failed to parse font variation settings.", e); + return false; + } + long builderPtr = nCreateFontVariationBuilder(axes.size()); + for (int i = 0; i < axes.size(); ++i) { + FontVariationAxis axis = axes.get(i); + nAddFontVariationToBuilder( + builderPtr, axis.getOpenTypeTagValue(), axis.getStyleValue()); + } + nSetFontVariationOverride(mNativePaint, builderPtr); + mFontVariationOverride = fontVariationOverride; + return true; + } + + /** + * Gets the current font variation override value. + * + * @return a current font variation override value. + */ + @FlaggedApi(FLAG_TYPEFACE_REDESIGN_READONLY) + public @Nullable String getFontVariationOverride() { + return mFontVariationOverride; + } + + /** * Get the current value of start hyphen edit. * * The default value is 0 which is equivalent to {@link #START_HYPHEN_EDIT_NO_EDIT}. diff --git a/keystore/java/android/security/keystore/KeyStoreManager.java b/keystore/java/android/security/keystore/KeyStoreManager.java index 740ccb53a691..13f1a72469c2 100644 --- a/keystore/java/android/security/keystore/KeyStoreManager.java +++ b/keystore/java/android/security/keystore/KeyStoreManager.java @@ -312,9 +312,11 @@ public final class KeyStoreManager { * When passed into getSupplementaryAttestationInfo, getSupplementaryAttestationInfo returns the * DER-encoded structure corresponding to the `Modules` schema described in the KeyMint HAL's * KeyCreationResult.aidl. The SHA-256 hash of this encoded structure is what's included with - * the tag in attestations. + * the tag in attestations. To ensure the returned encoded structure is the one attested to, + * clients should verify its SHA-256 hash matches the one in the attestation. Note that the + * returned structure can vary between boots. */ - // TODO(b/369375199): Replace with Tag.MODULE_HASH when flagging is removed. + // TODO(b/380020528): Replace with Tag.MODULE_HASH when KeyMint V4 is frozen. public static final int MODULE_HASH = TagType.BYTES | 724; /** diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml index b2ac640a468d..4f1cd9780f8b 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -26,6 +26,7 @@ <uses-permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" /> <uses-permission android:name="android.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION" /> <uses-permission android:name="android.permission.MANAGE_KEY_GESTURES" /> + <uses-permission android:name="android.permission.MANAGE_DISPLAYS" /> <application> <activity diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt index d15fbed409b8..23498de72481 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt @@ -32,9 +32,7 @@ enum class DesktopModeTransitionSource : Parcelable { /** Transitions with source unknown. */ UNKNOWN; - override fun describeContents(): Int { - return 0 - } + override fun describeContents(): Int = 0 override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeString(name) @@ -44,9 +42,8 @@ enum class DesktopModeTransitionSource : Parcelable { @JvmField val CREATOR = object : Parcelable.Creator<DesktopModeTransitionSource> { - override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource { - return parcel.readString()?.let { valueOf(it) } ?: UNKNOWN - } + override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource = + parcel.readString()?.let { valueOf(it) } ?: UNKNOWN override fun newArray(size: Int) = arrayOfNulls<DesktopModeTransitionSource>(size) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java index 8cd7b0f48003..82ef00e46e8c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java @@ -16,6 +16,7 @@ package com.android.wm.shell.appzoomout; +import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import android.app.ActivityManager; @@ -100,6 +101,7 @@ public class AppZoomOutController implements RemoteCallable<AppZoomOutController mDisplayController.addDisplayWindowListener(mDisplaysChangedListener); mDisplayController.addDisplayChangingController(this); + updateDisplayLayout(mContext.getDisplayId()); mDisplayAreaOrganizer.registerOrganizer(); } @@ -135,7 +137,9 @@ public class AppZoomOutController implements RemoteCallable<AppZoomOutController public void onDisplayChange(int displayId, int fromRotation, int toRotation, @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) { // TODO: verify if there is synchronization issues. - mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation); + if (toRotation != ROTATION_UNDEFINED) { + mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation); + } } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index f532be6b8277..72be066fc7a7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayTopology; import android.os.RemoteException; import android.util.ArraySet; import android.util.Size; @@ -34,6 +35,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; +import com.android.window.flags.Flags; import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellInit; @@ -54,6 +56,7 @@ public class DisplayController { private final ShellExecutor mMainExecutor; private final Context mContext; private final IWindowManager mWmService; + private final DisplayManager mDisplayManager; private final DisplayChangeController mChangeController; private final IDisplayWindowListener mDisplayContainerListener; @@ -61,10 +64,11 @@ public class DisplayController { private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>(); public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit, - ShellExecutor mainExecutor) { + ShellExecutor mainExecutor, DisplayManager displayManager) { mMainExecutor = mainExecutor; mContext = context; mWmService = wmService; + mDisplayManager = displayManager; // TODO: Inject this instead mChangeController = new DisplayChangeController(mWmService, shellInit, mainExecutor); mDisplayContainerListener = new DisplayWindowListenerImpl(); @@ -74,7 +78,7 @@ public class DisplayController { } /** - * Initializes the window listener. + * Initializes the window listener and the topology listener. */ public void onInit() { try { @@ -82,6 +86,12 @@ public class DisplayController { for (int i = 0; i < displayIds.length; i++) { onDisplayAdded(displayIds[i]); } + + if (Flags.enableConnectedDisplaysWindowDrag()) { + mDisplayManager.registerTopologyListener(mMainExecutor, + this::onDisplayTopologyChanged); + onDisplayTopologyChanged(mDisplayManager.getDisplayTopology()); + } } catch (RemoteException e) { throw new RuntimeException("Unable to register display controller"); } @@ -91,8 +101,7 @@ public class DisplayController { * Gets a display by id from DisplayManager. */ public Display getDisplay(int displayId) { - final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); - return displayManager.getDisplay(displayId); + return mDisplayManager.getDisplay(displayId); } /** @@ -221,6 +230,14 @@ public class DisplayController { } } + private void onDisplayTopologyChanged(DisplayTopology topology) { + // TODO(b/381472611): Call DisplayTopology#getCoordinates and update values in + // DisplayLayout when DM code is ready. + for (int i = 0; i < mDisplayChangedListeners.size(); ++i) { + mDisplayChangedListeners.get(i).onTopologyChanged(); + } + } + private void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { synchronized (mDisplays) { final DisplayRecord dr = mDisplays.get(displayId); @@ -408,5 +425,10 @@ public class DisplayController { */ default void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, Set<Rect> unrestricted) {} + + /** + * Called when the display topology has changed. + */ + default void onTopologyChanged() {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java index b6a1686bd087..4973a6f16409 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java @@ -31,7 +31,9 @@ import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.graphics.Insets; +import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.RectF; import android.os.SystemProperties; import android.provider.Settings; import android.util.DisplayMetrics; @@ -71,9 +73,12 @@ public class DisplayLayout { public static final int NAV_BAR_RIGHT = 1 << 1; public static final int NAV_BAR_BOTTOM = 1 << 2; + private static final String TAG = "DisplayLayout"; + private int mUiMode; private int mWidth; private int mHeight; + private RectF mGlobalBoundsDp; private DisplayCutout mCutout; private int mRotation; private int mDensityDpi; @@ -109,6 +114,7 @@ public class DisplayLayout { return mUiMode == other.mUiMode && mWidth == other.mWidth && mHeight == other.mHeight + && Objects.equals(mGlobalBoundsDp, other.mGlobalBoundsDp) && Objects.equals(mCutout, other.mCutout) && mRotation == other.mRotation && mDensityDpi == other.mDensityDpi @@ -127,8 +133,8 @@ public class DisplayLayout { @Override public int hashCode() { - return Objects.hash(mUiMode, mWidth, mHeight, mCutout, mRotation, mDensityDpi, - mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar, + return Objects.hash(mUiMode, mWidth, mHeight, mGlobalBoundsDp, mCutout, mRotation, + mDensityDpi, mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar, mNavBarFrameHeight, mTaskbarFrameHeight, mAllowSeamlessRotationDespiteNavBarMoving, mNavigationBarCanMove, mReverseDefaultRotation, mInsetsState); } @@ -170,6 +176,7 @@ public class DisplayLayout { mUiMode = dl.mUiMode; mWidth = dl.mWidth; mHeight = dl.mHeight; + mGlobalBoundsDp = dl.mGlobalBoundsDp; mCutout = dl.mCutout; mRotation = dl.mRotation; mDensityDpi = dl.mDensityDpi; @@ -193,6 +200,7 @@ public class DisplayLayout { mRotation = info.rotation; mCutout = info.displayCutout; mDensityDpi = info.logicalDensityDpi; + mGlobalBoundsDp = new RectF(0, 0, pxToDp(mWidth), pxToDp(mHeight)); mHasNavigationBar = hasNavigationBar; mHasStatusBar = hasStatusBar; mAllowSeamlessRotationDespiteNavBarMoving = res.getBoolean( @@ -255,6 +263,11 @@ public class DisplayLayout { recalcInsets(res); } + /** Update the global bounds of this layout, in DP. */ + public void setGlobalBoundsDp(RectF bounds) { + mGlobalBoundsDp = bounds; + } + /** Get this layout's non-decor insets. */ public Rect nonDecorInsets() { return mNonDecorInsets; @@ -265,16 +278,21 @@ public class DisplayLayout { return mStableInsets; } - /** Get this layout's width. */ + /** Get this layout's width in pixels. */ public int width() { return mWidth; } - /** Get this layout's height. */ + /** Get this layout's height in pixels. */ public int height() { return mHeight; } + /** Get this layout's global bounds in the multi-display coordinate system in DP. */ + public RectF globalBoundsDp() { + return mGlobalBoundsDp; + } + /** Get this layout's display rotation. */ public int rotation() { return mRotation; @@ -486,4 +504,48 @@ public class DisplayLayout { ? R.dimen.navigation_bar_frame_height_landscape : R.dimen.navigation_bar_frame_height); } + + /** + * Converts a pixel value to a density-independent pixel (dp) value. + * + * @param px The pixel value to convert. + * @return The equivalent value in DP units. + */ + public float pxToDp(Number px) { + return px.floatValue() * DisplayMetrics.DENSITY_DEFAULT / mDensityDpi; + } + + /** + * Converts a density-independent pixel (dp) value to a pixel value. + * + * @param dp The DP value to convert. + * @return The equivalent value in pixel units. + */ + public float dpToPx(Number dp) { + return dp.floatValue() * mDensityDpi / DisplayMetrics.DENSITY_DEFAULT; + } + + /** + * Converts local pixel coordinates on this layout to global DP coordinates. + * + * @param xPx The x-coordinate in pixels, relative to the layout's origin. + * @param yPx The y-coordinate in pixels, relative to the layout's origin. + * @return A PointF object representing the coordinates in global DP units. + */ + public PointF localPxToGlobalDp(Number xPx, Number yPx) { + return new PointF(mGlobalBoundsDp.left + pxToDp(xPx), + mGlobalBoundsDp.top + pxToDp(yPx)); + } + + /** + * Converts global DP coordinates to local pixel coordinates on this layout. + * + * @param xDp The x-coordinate in global DP units. + * @param yDp The y-coordinate in global DP units. + * @return A PointF object representing the coordinates in local pixel units on this layout. + */ + public PointF globalDpToLocalPx(Number xDp, Number yDp) { + return new PointF(dpToPx(xDp.floatValue() - mGlobalBoundsDp.left), + dpToPx(yDp.floatValue() - mGlobalBoundsDp.top)); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index ab3c33ec7e43..cbbe8a2b5613 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -25,6 +25,7 @@ import android.annotation.NonNull; import android.app.ActivityTaskManager; import android.content.Context; import android.content.pm.PackageManager; +import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.SystemProperties; import android.provider.Settings; @@ -175,8 +176,9 @@ public abstract class WMShellBaseModule { static DisplayController provideDisplayController(Context context, IWindowManager wmService, ShellInit shellInit, - @ShellMainThread ShellExecutor mainExecutor) { - return new DisplayController(context, wmService, shellInit, mainExecutor); + @ShellMainThread ShellExecutor mainExecutor, + DisplayManager displayManager) { + return new DisplayController(context, wmService, shellInit, mainExecutor, displayManager); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt index ca02c72c174e..f6fd9679922a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt @@ -93,9 +93,8 @@ class DesktopModeDragAndDropTransitionHandler(private val transitions: Transitio return matchingChanges.first() } - private fun isValidTaskChange(change: TransitionInfo.Change): Boolean { - return change.taskInfo != null && change.taskInfo?.taskId != -1 - } + private fun isValidTaskChange(change: TransitionInfo.Change): Boolean = + change.taskInfo != null && change.taskInfo?.taskId != -1 override fun handleRequest( transition: IBinder, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt index e975b586c1ee..c09504ee3725 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -434,18 +434,14 @@ class DesktopModeLoggerTransitionObserver( visibleFreeformTaskInfos.set(taskInfo.taskId, taskInfo) } - private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo { - return this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change") - } + private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo = + this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change") - private fun TaskInfo.isFreeformWindow(): Boolean { - return this.windowingMode == WINDOWING_MODE_FREEFORM - } + private fun TaskInfo.isFreeformWindow(): Boolean = this.windowingMode == WINDOWING_MODE_FREEFORM - private fun TransitionInfo.isExitToRecentsTransition(): Boolean { - return this.type == WindowManager.TRANSIT_TO_FRONT && + private fun TransitionInfo.isExitToRecentsTransition(): Boolean = + this.type == WindowManager.TRANSIT_TO_FRONT && this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS - } companion object { @VisibleForTesting const val VISIBLE_TASKS_COUNTER_NAME = "desktop_mode_visible_tasks" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index dba8c9367654..cdfa14bbc4e2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -24,8 +24,8 @@ import java.io.PrintWriter class DesktopModeShellCommandHandler(private val controller: DesktopTasksController) : ShellCommandHandler.ShellCommandActionHandler { - override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean { - return when (args[0]) { + override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean = + when (args[0]) { "moveToDesktop" -> { if (!runMoveToDesktop(args, pw)) { pw.println("Task not found. Please enter a valid taskId.") @@ -47,7 +47,6 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl false } } - } private fun runMoveToDesktop(args: Array<String>, pw: PrintWriter): Boolean { if (args.size < 2) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt index 848d80ff4f0b..f29301d92292 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt @@ -41,49 +41,35 @@ sealed class DesktopTaskPosition { return Point(x, y.toInt()) } - override fun next(): DesktopTaskPosition { - return BottomRight - } + override fun next(): DesktopTaskPosition = BottomRight } data object BottomRight : DesktopTaskPosition() { - override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { - return Point(frame.right - window.width(), frame.bottom - window.height()) - } + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point = + Point(frame.right - window.width(), frame.bottom - window.height()) - override fun next(): DesktopTaskPosition { - return TopLeft - } + override fun next(): DesktopTaskPosition = TopLeft } data object TopLeft : DesktopTaskPosition() { - override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { - return Point(frame.left, frame.top) - } + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point = + Point(frame.left, frame.top) - override fun next(): DesktopTaskPosition { - return BottomLeft - } + override fun next(): DesktopTaskPosition = BottomLeft } data object BottomLeft : DesktopTaskPosition() { - override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { - return Point(frame.left, frame.bottom - window.height()) - } + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point = + Point(frame.left, frame.bottom - window.height()) - override fun next(): DesktopTaskPosition { - return TopRight - } + override fun next(): DesktopTaskPosition = TopRight } data object TopRight : DesktopTaskPosition() { - override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { - return Point(frame.right - window.width(), frame.top) - } + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point = + Point(frame.right - window.width(), frame.top) - override fun next(): DesktopTaskPosition { - return Center - } + override fun next(): DesktopTaskPosition = Center } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 6013648c9806..73d15270c811 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -1470,13 +1470,9 @@ class DesktopTasksController( } } - override fun getContext(): Context { - return context - } + override fun getContext(): Context = context - override fun getRemoteCallExecutor(): ShellExecutor { - return mainExecutor - } + override fun getRemoteCallExecutor(): ShellExecutor = mainExecutor override fun startAnimation( transition: IBinder, @@ -1662,11 +1658,10 @@ class DesktopTasksController( DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() && isTopActivityExemptFromDesktopWindowing(context, task) - private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean { - return ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() && + private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean = + ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() && TransitionUtil.isClosingType(request.type) && request.triggerTask != null - } /** Open an existing instance of an app. */ fun openInstance(callingTask: RunningTaskInfo, requestedTaskId: Int) { @@ -2185,11 +2180,10 @@ class DesktopTasksController( getFocusedFreeformTask(displayId)?.let { requestSplit(it, leftOrTop) } } - private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? { - return shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo -> + private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? = + shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo -> taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM } - } /** * Requests a task be transitioned from desktop to split select. Applies needed windowing @@ -2237,14 +2231,10 @@ class DesktopTasksController( } } - private fun getDefaultDensityDpi(): Int { - return context.resources.displayMetrics.densityDpi - } + private fun getDefaultDensityDpi(): Int = context.resources.displayMetrics.densityDpi /** Creates a new instance of the external interface to pass to another process. */ - private fun createExternalInterface(): ExternalInterfaceBinder { - return IDesktopModeImpl(this) - } + private fun createExternalInterface(): ExternalInterfaceBinder = IDesktopModeImpl(this) /** Get connection interface between sysui and shell */ fun asDesktopMode(): DesktopMode { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index c2dd4d28305b..e4a28e9efe60 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -138,11 +138,10 @@ class DesktopTasksLimiter( ) } - private fun getMinimizeChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? { - return info.changes.find { change -> + private fun getMinimizeChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? = + info.changes.find { change -> change.taskInfo?.taskId == taskId && change.mode == TRANSIT_TO_BACK } - } override fun onTransitionMerged(merged: IBinder, playing: IBinder) { if (activeTransitionTokensAndTasks.remove(merged) != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index 1380a9ca164f..91f10dc4faf5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -765,9 +765,8 @@ sealed class DragToDesktopTransitionHandler( transitionState = null } - private fun isSplitTask(taskId: Int): Boolean { - return splitScreenController.isTaskInSplitScreen(taskId) - } + private fun isSplitTask(taskId: Int): Boolean = + splitScreenController.isTaskInSplitScreen(taskId) private fun getOtherSplitTask(taskId: Int): Int? { val splitPos = splitScreenController.getSplitPosition(taskId) @@ -781,9 +780,8 @@ sealed class DragToDesktopTransitionHandler( return splitScreenController.getTaskInfo(otherTaskPos)?.taskId } - protected fun requireTransitionState(): TransitionState { - return transitionState ?: error("Expected non-null transition state") - } + protected fun requireTransitionState(): TransitionState = + transitionState ?: error("Expected non-null transition state") /** * Represents the layering (Z order) that will be given to any window based on its type during diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt index a47e9370b58d..5e84019b14f5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt @@ -156,13 +156,11 @@ class ToggleResizeDesktopTaskTransitionHandler( return matchingChanges.first() } - private fun isWallpaper(change: TransitionInfo.Change): Boolean { - return (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0 - } + private fun isWallpaper(change: TransitionInfo.Change): Boolean = + (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0 - private fun isValidTaskChange(change: TransitionInfo.Change): Boolean { - return change.taskInfo != null && change.taskInfo?.taskId != -1 - } + private fun isValidTaskChange(change: TransitionInfo.Change): Boolean = + change.taskInfo != null && change.taskInfo?.taskId != -1 companion object { private const val RESIZE_DURATION_MS = 300L diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java index 1e5e153fdfe1..d3de0f7c09b4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; +import android.hardware.display.DisplayManager; import android.view.IWindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -50,12 +51,14 @@ public class DisplayControllerTests extends ShellTestCase { private @Mock IWindowManager mWM; private @Mock ShellInit mShellInit; private @Mock ShellExecutor mMainExecutor; + private @Mock DisplayManager mDisplayManager; private DisplayController mController; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mController = new DisplayController(mContext, mWM, mShellInit, mMainExecutor); + mController = new DisplayController( + mContext, mWM, mShellInit, mMainExecutor, mDisplayManager); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java index d467b399ebbb..b0a455d1bcf8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java @@ -33,7 +33,9 @@ import static org.mockito.Mockito.when; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Insets; +import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.RectF; import android.view.DisplayCutout; import android.view.DisplayInfo; @@ -58,6 +60,7 @@ import org.mockito.quality.Strictness; @SmallTest public class DisplayLayoutTest extends ShellTestCase { private MockitoSession mMockitoSession; + private static final float DELTA = 0.1f; // Constant for assertion delta @Before public void setup() { @@ -130,6 +133,39 @@ public class DisplayLayoutTest extends ShellTestCase { assertEquals(new Rect(40, 0, 60, 0), dl.nonDecorInsets()); } + @Test + public void testDpPxConversion() { + int px = 100; + float dp = 53.33f; + int xPx = 100; + int yPx = 200; + float xDp = 164.33f; + float yDp = 328.66f; + + Resources res = createResources(40, 50, false); + DisplayInfo info = createDisplayInfo(1000, 1500, 0, ROTATION_0); + DisplayLayout dl = new DisplayLayout(info, res, false, false); + dl.setGlobalBoundsDp(new RectF(111f, 222f, 300f, 400f)); + + // Test pxToDp + float resultDp = dl.pxToDp(px); + assertEquals(dp, resultDp, DELTA); + + // Test dpToPx + float resultPx = dl.dpToPx(dp); + assertEquals(px, resultPx, DELTA); + + // Test localPxToGlobalDp + PointF resultGlobalDp = dl.localPxToGlobalDp(xPx, yPx); + assertEquals(xDp, resultGlobalDp.x, DELTA); + assertEquals(yDp, resultGlobalDp.y, DELTA); + + // Test globalDpToLocalPx + PointF resultLocalPx = dl.globalDpToLocalPx(xDp, yDp); + assertEquals(xPx, resultLocalPx.x, DELTA); + assertEquals(yPx, resultLocalPx.y, DELTA); + } + private Resources createResources(int navLand, int navPort, boolean navMoves) { Configuration cfg = new Configuration(); cfg.uiMode = UI_MODE_TYPE_NORMAL; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt index 9b24c1c06cec..eb6f1d75e6ca 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt @@ -501,13 +501,12 @@ class DesktopModeEventLoggerTest : ShellTestCase() { } } - private fun createTaskInfo(): RunningTaskInfo { - return TestRunningTaskInfoBuilder() + private fun createTaskInfo(): RunningTaskInfo = + TestRunningTaskInfoBuilder() .setTaskId(TASK_ID) .setUid(TASK_UID) .setBounds(Rect(TASK_X, TASK_Y, TASK_WIDTH, TASK_HEIGHT)) .build() - } private fun verifyNoLogging() { verify( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 4bb743079861..95ed8b4d4511 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -353,8 +353,8 @@ class DesktopTasksControllerTest : ShellTestCase() { taskRepository = userRepositories.current } - private fun createController(): DesktopTasksController { - return DesktopTasksController( + private fun createController() = + DesktopTasksController( context, shellInit, shellCommandHandler, @@ -388,7 +388,6 @@ class DesktopTasksControllerTest : ShellTestCase() { desktopWallpaperActivityTokenProvider, Optional.of(bubbleController), ) - } @After fun tearDown() { @@ -4958,13 +4957,12 @@ class DesktopTasksControllerTest : ShellTestCase() { return task } - private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo { + private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo = // active = false marks the task as non-visible; PiP window doesn't count as visible tasks - return setUpFreeformTask(active = false).apply { + setUpFreeformTask(active = false).apply { pictureInPictureParams = PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterEnabled).build() } - } private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { val task = createHomeTask(displayId) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt index 96ed214e7f88..622cb4cad363 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -458,8 +458,8 @@ class DesktopTasksTransitionObserverTest { } } - private fun createCloseTransition(task: RunningTaskInfo?): TransitionInfo { - return TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0).apply { + private fun createCloseTransition(task: RunningTaskInfo?) = + TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0).apply { addChange( Change(mock(), mock()).apply { mode = TRANSIT_CLOSE @@ -469,10 +469,9 @@ class DesktopTasksTransitionObserverTest { } ) } - } - private fun createToBackTransition(task: RunningTaskInfo?): TransitionInfo { - return TransitionInfo(TRANSIT_TO_BACK, /* flags= */ 0).apply { + private fun createToBackTransition(task: RunningTaskInfo?) = + TransitionInfo(TRANSIT_TO_BACK, /* flags= */ 0).apply { addChange( Change(mock(), mock()).apply { mode = TRANSIT_TO_BACK @@ -482,7 +481,6 @@ class DesktopTasksTransitionObserverTest { } ) } - } private fun createToFrontTransition(task: RunningTaskInfo?): TransitionInfo { return TransitionInfo(TRANSIT_TO_FRONT, 0 /* flags */).apply { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index 341df0299a97..bf9cf00050dc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -676,8 +676,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } } - private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo): TransitionInfo { - return TransitionInfo(type, /* flags= */ 0).apply { + private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo) = + TransitionInfo(type, /* flags= */ 0).apply { addChange( // Home. TransitionInfo.Change(mock(), homeTaskLeash).apply { parent = null @@ -700,7 +700,6 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } ) } - } private fun systemPropertiesKey(name: String) = "${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name" diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt index d410151b4602..5389c94bc15d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt @@ -43,7 +43,7 @@ import org.mockito.kotlin.verify */ @SmallTest @RunWith(AndroidJUnit4::class) -class UnhandledDragControllerTest : ShellTestCase() { +class GlobalDragListenerTest : ShellTestCase() { private val mIWindowManager = mock<IWindowManager>() private val mMainExecutor = mock<ShellExecutor>() @@ -74,7 +74,7 @@ class UnhandledDragControllerTest : ShellTestCase() { @Test fun onUnhandledDrop_noListener_expectNotifyUnhandled() { // Simulate an unhandled drop - val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, null, null, null, + val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, 0, null, null, null, null, null, false) val wmCallback = mock<IUnhandledDragCallback>() mController.onUnhandledDrop(dropEvent, wmCallback) @@ -98,7 +98,7 @@ class UnhandledDragControllerTest : ShellTestCase() { // Simulate an unhandled drop val dragSurface = mock<SurfaceControl>() - val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, null, null, null, + val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, 0, null, null, null, dragSurface, null, false) val wmCallback = mock<IUnhandledDragCallback>() mController.onUnhandledDrop(dropEvent, wmCallback) diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 76ad2acccf89..5e71d3360f39 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -34,13 +34,6 @@ flag { } flag { - name: "high_contrast_text_luminance" - namespace: "accessibility" - description: "Use luminance to determine how to make text more high contrast, instead of RGB heuristic" - bug: "186567103" -} - -flag { name: "high_contrast_text_small_text_rect" namespace: "accessibility" description: "Draw a solid rectangle background behind text instead of a stroke outline" diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index e13e136550ca..e05c3d695463 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -34,9 +34,6 @@ namespace flags = com::android::graphics::hwui::flags; #else namespace flags { -constexpr bool high_contrast_text_luminance() { - return false; -} constexpr bool high_contrast_text_small_text_rect() { return false; } @@ -114,15 +111,10 @@ public: if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) { // high contrast draw path int color = paint.getColor(); - bool darken; - // This equation should match the one in core/java/android/text/Layout.java - if (flags::high_contrast_text_luminance()) { - uirenderer::Lab lab = uirenderer::sRGBToLab(color); - darken = lab.L <= 50; - } else { - int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color); - darken = channelSum < (128 * 3); - } + // LINT.IfChange(hct_darken) + uirenderer::Lab lab = uirenderer::sRGBToLab(color); + bool darken = lab.L <= 50; + // LINT.ThenChange(/core/java/android/text/Layout.java:hct_darken) // outline gDrawTextBlobMode = DrawTextBlobMode::HctOutline; diff --git a/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm index b384a2418ff2..b0308b5dccd4 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm @@ -120,78 +120,90 @@ key EQUALS { key Q { label: 'Q' - base, capslock+shift: 'q' + base: 'q' shift, capslock: 'Q' + shift+capslock: 'q' } key W { label: 'W' - base, capslock+shift: 'w' + base: 'w' shift, capslock: 'W' + shift+capslock: 'w' } key E { label: 'E' - base, capslock+shift: 'e' + base: 'e' shift, capslock: 'E' + shift+capslock: 'e' ralt: '\u20ac' } key R { label: 'R' - base, capslock+shift: 'r' + base: 'r' shift, capslock: 'R' + shift+capslock: 'r' } key T { label: 'T' - base, capslock+shift: 't' + base: 't' shift, capslock: 'T' + shift+capslock: 't' } key Y { label: 'Y' - base, capslock+shift: 'y' + base: 'y' shift, capslock: 'Y' + shift+capslock: 'y' } key U { label: 'U' - base, capslock+shift: 'u' + base: 'u' shift, capslock: 'U' + shift+capslock: 'u' } key I { label: 'I' - base, capslock+shift: 'i' + base: 'i' shift, capslock: 'I' + shift+capslock: 'i' } key O { label: 'O' - base, capslock+shift: 'o' + base: 'o' shift, capslock: 'O' + shift+capslock: 'o' } key P { label: 'P' - base, capslock+shift: 'p' + base: 'p' shift, capslock: 'P' + shift+capslock: 'p' ralt: '\u00a7' } key LEFT_BRACKET { label: '\u0102' - base, capslock+shift: '\u0103' + base: '\u0103' shift, capslock: '\u0102' + shift+capslock: '\u0103' ralt: '[' ralt+shift: '{' } key RIGHT_BRACKET { label: '\u00ce' - base, capslock+shift: '\u00ee' + base: '\u00ee' shift, capslock: '\u00ce' + shift+capslock: '\u00ee' ralt: ']' ralt+shift: '}' } @@ -200,21 +212,24 @@ key RIGHT_BRACKET { key A { label: 'A' - base, capslock+shift: 'a' + base: 'a' shift, capslock: 'A' + shift+capslock: 'a' } key S { label: 'S' - base, capslock+shift: 's' + base: 's' shift, capslock: 'S' + shift+capslock: 's' ralt: '\u00df' } key D { label: 'D' - base, capslock+shift: 'd' + base: 'd' shift, capslock: 'D' + shift+capslock: 'd' ralt: '\u0111' ralt+shift, ralt+capslock: '\u0110' ralt+shift+capslock: '\u0111' @@ -222,38 +237,44 @@ key D { key F { label: 'F' - base, capslock+shift: 'f' + base: 'f' shift, capslock: 'F' + shift+capslock: 'f' } key G { label: 'G' - base, capslock+shift: 'g' + base: 'g' shift, capslock: 'G' + shift+capslock: 'g' } key H { label: 'H' - base, capslock+shift: 'h' + base: 'h' shift, capslock: 'H' + shift+capslock: 'h' } key J { label: 'J' - base, capslock+shift: 'j' + base: 'j' shift, capslock: 'J' + shift+capslock: 'j' } key K { label: 'K' - base, capslock+shift: 'k' + base: 'k' shift, capslock: 'K' + shift+capslock: 'k' } key L { label: 'L' - base, capslock+shift: 'l' + base: 'l' shift, capslock: 'L' + shift+capslock: 'l' ralt: '\u0142' ralt+shift, ralt+capslock: '\u0141' ralt+shift+capslock: '\u0142' @@ -261,24 +282,27 @@ key L { key SEMICOLON { label: '\u0218' - base, capslock+shift: '\u0219' + base: '\u0219' shift, capslock: '\u0218' + shift+capslock: '\u0219' ralt: ';' ralt+shift: ':' } key APOSTROPHE { label: '\u021a' - base, capslock+shift: '\u021b' + base: '\u021b' shift, capslock: '\u021a' + shift+capslock: '\u021b' ralt: '\'' ralt+shift: '\u0022' } key BACKSLASH { label: '\u00c2' - base, capslock+shift: '\u00e2' + base: '\u00e2' shift, capslock: '\u00c2' + shift+capslock: '\u00e2' ralt: '\\' ralt+shift: '|' } @@ -293,45 +317,52 @@ key PLUS { key Z { label: 'Z' - base, capslock+shift: 'z' + base: 'z' shift, capslock: 'Z' + shift+capslock: 'z' } key X { label: 'X' - base, capslock+shift: 'x' + base: 'x' shift, capslock: 'X' + shift+capslock: 'x' } key C { label: 'C' - base, capslock+shift: 'c' + base: 'c' shift, capslock: 'C' + shift+capslock: 'c' ralt: '\u00a9' } key V { label: 'V' - base, capslock+shift: 'v' + base: 'v' shift, capslock: 'V' + shift+capslock: 'v' } key B { label: 'B' - base, capslock+shift: 'b' + base: 'b' shift, capslock: 'B' + shift+capslock: 'b' } key N { label: 'N' - base, capslock+shift: 'n' + base: 'n' shift, capslock: 'N' + shift+capslock: 'n' } key M { label: 'M' - base, capslock+shift: 'm' + base: 'm' shift, capslock: 'M' + shift+capslock: 'm' } key COMMA { diff --git a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm index 6fa54f9d052f..9df78c9af923 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm @@ -104,149 +104,173 @@ key EQUALS { key Q { label: '\u0409' - base, capslock+shift: '\u0459' + base: '\u0459' shift, capslock: '\u0409' + shift+capslock: '\u0459' } key W { label: '\u040a' - base, capslock+shift: '\u045a' + base: '\u045a' shift, capslock: '\u040a' + shift+capslock: '\u045a' } key E { label: '\u0415' - base, capslock+shift: '\u0435' + base: '\u0435' shift, capslock: '\u0415' + shift+capslock: '\u0435' ralt: '\u20ac' } key R { label: '\u0420' - base, capslock+shift: '\u0440' + base: '\u0440' shift, capslock: '\u0420' + shift+capslock: '\u0440' } key T { label: '\u0422' - base, capslock+shift: '\u0442' + base: '\u0442' shift, capslock: '\u0422' + shift+capslock: '\u0442' } key Y { label: '\u0417' - base, capslock+shift: '\u0437' + base: '\u0437' shift, capslock: '\u0417' + shift+capslock: '\u0437' } key U { label: '\u0423' - base, capslock+shift: '\u0443' + base: '\u0443' shift, capslock: '\u0423' + shift+capslock: '\u0443' } key I { label: '\u0418' - base, capslock+shift: '\u0438' + base: '\u0438' shift, capslock: '\u0418' + shift+capslock: '\u0438' } key O { label: '\u041e' - base, capslock+shift: '\u043e' + base: '\u043e' shift, capslock: '\u041e' + shift+capslock: '\u043e' } key P { label: '\u041f' - base, capslock+shift: '\u043f' + base: '\u043f' shift, capslock: '\u041f' + shift+capslock: '\u043f' } key LEFT_BRACKET { label: '\u0428' - base, capslock+shift: '\u0448' + base: '\u0448' shift, capslock: '\u0428' + shift+capslock: '\u0448' } key RIGHT_BRACKET { label: '\u0402' - base, capslock+shift: '\u0452' + base: '\u0452' shift, capslock: '\u0402' + shift+capslock: '\u0452' } ### ROW 3 key A { label: '\u0410' - base, capslock+shift: '\u0430' + base: '\u0430' shift, capslock: '\u0410' + shift+capslock: '\u0430' } key S { label: '\u0421' - base, capslock+shift: '\u0441' + base: '\u0441' shift, capslock: '\u0421' + shift+capslock: '\u0441' } key D { label: '\u0414' - base, capslock+shift: '\u0434' + base: '\u0434' shift, capslock: '\u0414' + shift+capslock: '\u0434' } key F { label: '\u0424' - base, capslock+shift: '\u0444' + base: '\u0444' shift, capslock: '\u0424' + shift+capslock: '\u0444' } key G { label: '\u0413' - base, capslock+shift: '\u0433' + base: '\u0433' shift, capslock: '\u0413' + shift+capslock: '\u0433' } key H { label: '\u0425' - base, capslock+shift: '\u0445' + base: '\u0445' shift, capslock: '\u0425' + shift+capslock: '\u0445' } key J { label: '\u0408' - base, capslock+shift: '\u0458' + base: '\u0458' shift, capslock: '\u0408' + shift+capslock: '\u0458' } key K { label: '\u041a' - base, capslock+shift: '\u043a' + base: '\u043a' shift, capslock: '\u041a' + shift+capslock: '\u043a' } key L { label: '\u041b' - base, capslock+shift: '\u043b' + base: '\u043b' shift, capslock: '\u041b' + shift+capslock: '\u043b' } key SEMICOLON { label: '\u0427' - base, capslock+shift: '\u0447' + base: '\u0447' shift, capslock: '\u0427' + shift+capslock: '\u0447' } key APOSTROPHE { label: '\u040b' - base, capslock+shift: '\u045b' + base: '\u045b' shift, capslock: '\u040b' + shift+capslock: '\u045b' } key BACKSLASH { label: '\u0416' - base, capslock+shift: '\u0436' + base: '\u0436' shift, capslock: '\u0416' + shift+capslock: '\u0436' } ### ROW 4 @@ -259,44 +283,51 @@ key PLUS { key Z { label: '\u0405' - base, capslock+shift: '\u0455' + base: '\u0455' shift, capslock: '\u0405' + shift+capslock: '\u0455' } key X { label: '\u040f' - base, capslock+shift: '\u045f' + base: '\u045f' shift, capslock: '\u040f' + shift+capslock: '\u045f' } key C { label: '\u0426' - base, capslock+shift: '\u0446' + base: '\u0446' shift, capslock: '\u0426' + shift+capslock: '\u0446' } key V { label: '\u0412' - base, capslock+shift: '\u0432' + base: '\u0432' shift, capslock: '\u0412' + shift+capslock: '\u0432' } key B { label: '\u0411' - base, capslock+shift: '\u0431' + base: '\u0431' shift, capslock: '\u0411' + shift+capslock: '\u0431' } key N { label: '\u041d' - base, capslock+shift: '\u043d' + base: '\u043d' shift, capslock: '\u041d' + shift+capslock: '\u043d' } key M { label: '\u041c' - base, capslock+shift: '\u043c' + base: '\u043c' shift, capslock: '\u041c' + shift+capslock: '\u043c' } key COMMA { @@ -317,4 +348,4 @@ key SLASH { label: '-' base: '-' shift: '_' -}
\ No newline at end of file +} diff --git a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm index 8e4d7b147faa..4c8997b16a26 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm @@ -120,78 +120,90 @@ key EQUALS { key Q { label: 'Q' - base, capslock+shift: 'q' + base: 'q' shift, capslock: 'Q' + shift+capslock: 'q' ralt: '\\' } key W { label: 'W' - base, capslock+shift: 'w' + base: 'w' shift, capslock: 'W' + shift+capslock: 'w' ralt: '|' } key E { label: 'E' - base, capslock+shift: 'e' + base: 'e' shift, capslock: 'E' + shift+capslock: 'e' ralt: '\u20ac' } key R { label: 'R' - base, capslock+shift: 'r' + base: 'r' shift, capslock: 'R' + shift+capslock: 'r' } key T { label: 'T' - base, capslock+shift: 't' + base: 't' shift, capslock: 'T' + shift+capslock: 't' } key Z { label: 'Z' - base, capslock+shift: 'z' + base: 'z' shift, capslock: 'Z' + shift+capslock: 'z' } key U { label: 'U' - base, capslock+shift: 'u' + base: 'u' shift, capslock: 'U' + shift+capslock: 'u' } key I { label: 'I' - base, capslock+shift: 'i' + base: 'i' shift, capslock: 'I' + shift+capslock: 'i' } key O { label: 'O' - base, capslock+shift: 'o' + base: 'o' shift, capslock: 'O' + shift+capslock: 'o' } key P { label: 'P' - base, capslock+shift: 'p' + base: 'p' shift, capslock: 'P' + shift+capslock: 'p' } key LEFT_BRACKET { label: '\u0160' - base, capslock+shift: '\u0161' + base: '\u0161' shift, capslock: '\u0160' + shift+capslock: '\u0161' ralt: '\u00f7' } key RIGHT_BRACKET { label: '\u0110' - base, capslock+shift: '\u0111' + base: '\u0111' shift, capslock: '\u0110' + shift+capslock: '\u0111' ralt: '\u00d7' } @@ -199,79 +211,91 @@ key RIGHT_BRACKET { key A { label: 'A' - base, capslock+shift: 'a' + base: 'a' shift, capslock: 'A' + shift+capslock: 'a' } key S { label: 'S' - base, capslock+shift: 's' + base: 's' shift, capslock: 'S' + shift+capslock: 's' } key D { label: 'D' - base, capslock+shift: 'd' + base: 'd' shift, capslock: 'D' + shift+capslock: 'd' } key F { label: 'F' - base, capslock+shift: 'f' + base: 'f' shift, capslock: 'F' + shift+capslock: 'f' ralt: '[' } key G { label: 'G' - base, capslock+shift: 'g' + base: 'g' shift, capslock: 'G' + shift+capslock: 'g' ralt: ']' } key H { label: 'H' - base, capslock+shift: 'h' + base: 'h' shift, capslock: 'H' + shift+capslock: 'h' } key J { label: 'J' - base, capslock+shift: 'j' + base: 'j' shift, capslock: 'J' + shift+capslock: 'j' } key K { label: 'K' - base, capslock+shift: 'k' + base: 'k' shift, capslock: 'K' + shift+capslock: 'k' ralt: '\u0142' } key L { label: 'L' - base, capslock+shift: 'l' + base: 'l' shift, capslock: 'L' + shift+capslock: 'l' ralt: '\u0141' } key SEMICOLON { label: '\u010c' - base, capslock+shift: '\u010d' + base: '\u010d' shift, capslock: '\u010c' + shift+capslock: '\u010d' } key APOSTROPHE { label: '\u0106' - base, capslock+shift: '\u0107' + base: '\u0107' shift, capslock: '\u0106' + shift+capslock: '\u0107' ralt: '\u00df' } key BACKSLASH { label: '\u017d' - base, capslock+shift: '\u017e' + base: '\u017e' shift, capslock: '\u017d' + shift+capslock: '\u017e' ralt: '\u00a4' } @@ -285,47 +309,54 @@ key PLUS { key Y { label: 'Y' - base, capslock+shift: 'y' + base: 'y' shift, capslock: 'Y' + shift+capslock: 'y' } key X { label: 'X' - base, capslock+shift: 'x' + base: 'x' shift, capslock: 'X' + shift+capslock: 'x' } key C { label: 'C' - base, capslock+shift: 'c' + base: 'c' shift, capslock: 'C' + shift+capslock: 'c' } key V { label: 'V' - base, capslock+shift: 'v' + base: 'v' shift, capslock: 'V' + shift+capslock: 'v' ralt: '@' } key B { label: 'B' - base, capslock+shift: 'b' + base: 'b' shift, capslock: 'B' + shift+capslock: 'b' ralt: '{' } key N { label: 'N' - base, capslock+shift: 'n' + base: 'n' shift, capslock: 'N' + shift+capslock: 'n' ralt: '}' } key M { label: 'M' - base, capslock+shift: 'm' + base: 'm' shift, capslock: 'M' + shift+capslock: 'm' ralt: '\u00a7' } @@ -347,4 +378,4 @@ key MINUS { label: '-' base: '-' shift: '_' -}
\ No newline at end of file +} diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt index 145fabea52af..ac36b08512e8 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt @@ -39,5 +39,7 @@ annotation class DataChangeReason { const val RESTORE = 3 /** Data is synced from another profile (e.g. personal profile to work profile). */ const val SYNC_ACROSS_PROFILES = 4 + + fun isDataChange(reason: Int): Boolean = reason in UNKNOWN..SYNC_ACROSS_PROFILES } } diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt index 3c870acf2291..ea795542a5f6 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt @@ -34,6 +34,8 @@ import com.android.settingslib.metadata.PreferenceRestrictionProvider import com.android.settingslib.metadata.PreferenceScreenRegistry import com.android.settingslib.metadata.RangeValue import com.android.settingslib.metadata.ReadWritePermit +import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY +import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY /** Request to set preference value. */ data class PreferenceSetterRequest( @@ -187,6 +189,8 @@ fun <T> PersistentPreference<T>.evalWritePermit( callingUid: Int, ): Int = when { + sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY -> + ReadWritePermit.DISALLOW getWritePermissions(context)?.check(context, callingPid, callingUid) == false -> ReadWritePermit.REQUIRE_APP_PERMISSION else -> getWritePermit(context, value, callingPid, callingUid) diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt index e5bf41f87999..83725aaec377 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt @@ -44,6 +44,24 @@ annotation class ReadWritePermit { } } +/** The reason of preference change. */ +@IntDef( + PreferenceChangeReason.VALUE, + PreferenceChangeReason.STATE, + PreferenceChangeReason.DEPENDENT, +) +@Retention(AnnotationRetention.SOURCE) +annotation class PreferenceChangeReason { + companion object { + /** Preference value is changed. */ + const val VALUE = 1000 + /** Preference state (title/summary, enable state, etc.) is changed. */ + const val STATE = 1001 + /** Dependent preference state is changed. */ + const val DEPENDENT = 1002 + } +} + /** Indicates how sensitive of the data. */ @Retention(AnnotationRetention.SOURCE) @Target(AnnotationTarget.TYPE) diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt index 91abd8b4c9e9..8358ab921fb6 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt @@ -25,12 +25,14 @@ import androidx.preference.Preference import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceGroup import androidx.preference.PreferenceScreen +import com.android.settingslib.datastore.DataChangeReason import com.android.settingslib.datastore.HandlerExecutor import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyedDataObservable import com.android.settingslib.datastore.KeyedObservable import com.android.settingslib.datastore.KeyedObserver import com.android.settingslib.metadata.PersistentPreference +import com.android.settingslib.metadata.PreferenceChangeReason import com.android.settingslib.metadata.PreferenceHierarchy import com.android.settingslib.metadata.PreferenceHierarchyNode import com.android.settingslib.metadata.PreferenceLifecycleContext @@ -73,7 +75,7 @@ class PreferenceScreenBindingHelper( ?.keyValueStore override fun notifyPreferenceChange(key: String) = - notifyChange(key, CHANGE_REASON_STATE) + notifyChange(key, PreferenceChangeReason.STATE) @Suppress("DEPRECATION") override fun startActivityForResult( @@ -91,7 +93,13 @@ class PreferenceScreenBindingHelper( private val preferenceObserver: KeyedObserver<String?> private val storageObserver = - KeyedObserver<String> { key, _ -> notifyChange(key, CHANGE_REASON_VALUE) } + KeyedObserver<String> { key, reason -> + if (DataChangeReason.isDataChange(reason)) { + notifyChange(key, PreferenceChangeReason.VALUE) + } else { + notifyChange(key, PreferenceChangeReason.STATE) + } + } init { val preferencesBuilder = ImmutableMap.builder<String, PreferenceHierarchyNode>() @@ -148,7 +156,7 @@ class PreferenceScreenBindingHelper( } // check reason to avoid potential infinite loop - if (reason != CHANGE_REASON_DEPENDENT) { + if (reason != PreferenceChangeReason.DEPENDENT) { notifyDependents(key, mutableSetOf()) } } @@ -157,7 +165,7 @@ class PreferenceScreenBindingHelper( private fun notifyDependents(key: String, notifiedKeys: MutableSet<String>) { if (!notifiedKeys.add(key)) return for (dependency in dependencies[key]) { - notifyChange(dependency, CHANGE_REASON_DEPENDENT) + notifyChange(dependency, PreferenceChangeReason.DEPENDENT) notifyDependents(dependency, notifiedKeys) } } @@ -210,13 +218,6 @@ class PreferenceScreenBindingHelper( } companion object { - /** Preference value is changed. */ - const val CHANGE_REASON_VALUE = 0 - /** Preference state (title/summary, enable state, etc.) is changed. */ - const val CHANGE_REASON_STATE = 1 - /** Dependent preference state is changed. */ - const val CHANGE_REASON_DEPENDENT = 2 - /** Updates preference screen that has incomplete hierarchy. */ @JvmStatic fun bind(preferenceScreen: PreferenceScreen) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java index 6be4336178eb..155c7e6530aa 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java @@ -21,6 +21,7 @@ import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_ALL; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; +import android.annotation.CallbackExecutor; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothCsipSetCoordinator; @@ -39,6 +40,7 @@ import com.android.settingslib.R; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executor; public class LeAudioProfile implements LocalBluetoothProfile { private static final String TAG = "LeAudioProfile"; @@ -317,6 +319,78 @@ public class LeAudioProfile implements LocalBluetoothProfile { return mService.getAudioLocation(device); } + /** + * Sets the fallback group id when broadcast switches to unicast. + * + * @param groupId the target fallback group id + */ + public void setBroadcastToUnicastFallbackGroup(int groupId) { + if (mService == null) { + Log.w(TAG, "Proxy not attached to service. Cannot set fallback group: " + groupId); + return; + } + + mService.setBroadcastToUnicastFallbackGroup(groupId); + } + + /** + * Gets the fallback group id when broadcast switches to unicast. + * + * @return current fallback group id + */ + public int getBroadcastToUnicastFallbackGroup() { + if (mService == null) { + Log.w(TAG, "Proxy not attached to service. Cannot get fallback group."); + return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; + } + return mService.getBroadcastToUnicastFallbackGroup(); + } + + /** + * Registers a {@link BluetoothLeAudio.Callback} that will be invoked during the + * operation of this profile. + * + * Repeated registration of the same <var>callback</var> object after the first call to this + * method will result with IllegalArgumentException being thrown, even when the + * <var>executor</var> is different. API caller would have to call + * {@link #unregisterCallback(BluetoothLeAudio.Callback)} with the same callback object + * before registering it again. + * + * @param executor an {@link Executor} to execute given callback + * @param callback user implementation of the {@link BluetoothLeAudio.Callback} + * @throws NullPointerException if a null executor, or callback is given, or + * IllegalArgumentException if the same <var>callback</var> is + * already registered. + */ + public void registerCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull BluetoothLeAudio.Callback callback) { + if (mService == null) { + Log.w(TAG, "Proxy not attached to service. Cannot register callback."); + return; + } + mService.registerCallback(executor, callback); + } + + /** + * Unregisters the specified {@link BluetoothLeAudio.Callback}. + * <p>The same {@link BluetoothLeAudio.Callback} object used when calling + * {@link #registerCallback(Executor, BluetoothLeAudio.Callback)} must be used. + * + * <p>Callbacks are automatically unregistered when application process goes away + * + * @param callback user implementation of the {@link BluetoothLeAudio.Callback} + * @throws NullPointerException when callback is null or IllegalArgumentException when no + * callback is registered + */ + public void unregisterCallback(@NonNull BluetoothLeAudio.Callback callback) { + if (mService == null) { + Log.w(TAG, "Proxy not attached to service. Cannot unregister callback."); + return; + } + mService.unregisterCallback(callback); + } + @RequiresApi(Build.VERSION_CODES.S) protected void finalize() { if (DEBUG) { diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 70d4cc2e4e26..9982710737ce 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1795,16 +1795,6 @@ flag { } flag { - name: "disable_shade_expands_on_trackpad_two_finger_swipe" - namespace: "systemui" - description: "Disables expansion of the shade via two finger swipe on a trackpad" - bug: "356804470" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "keyboard_shortcut_helper_shortcut_customizer" namespace: "systemui" description: "An implementation of shortcut customizations through shortcut helper." @@ -1905,6 +1895,16 @@ flag { } flag { + name: "disable_shade_trackpad_two_finger_swipe" + namespace: "systemui" + description: "Disables expansion of the shade via two finger swipe on a trackpad" + bug: "356804470" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "notification_magic_actions_treatment" namespace: "systemui" description: "Special UI treatment for magic actions" diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt index b0c7ac09551a..c8d3430bf54b 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt @@ -232,19 +232,21 @@ constructor( private val lifecycleListener = object : Listener { override fun onTransitionAnimationStart() { - listeners.forEach { it.onTransitionAnimationStart() } + LinkedHashSet(listeners).forEach { it.onTransitionAnimationStart() } } override fun onTransitionAnimationEnd() { - listeners.forEach { it.onTransitionAnimationEnd() } + LinkedHashSet(listeners).forEach { it.onTransitionAnimationEnd() } } override fun onTransitionAnimationProgress(linearProgress: Float) { - listeners.forEach { it.onTransitionAnimationProgress(linearProgress) } + LinkedHashSet(listeners).forEach { + it.onTransitionAnimationProgress(linearProgress) + } } override fun onTransitionAnimationCancelled() { - listeners.forEach { it.onTransitionAnimationCancelled() } + LinkedHashSet(listeners).forEach { it.onTransitionAnimationCancelled() } } } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt index 96401ce6e1c7..a27bf8af1806 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt @@ -444,7 +444,7 @@ private class NestedDraggableNode( val left = available - consumed val postConsumed = nestedScrollDispatcher.dispatchPostScroll( - consumed = preConsumed + consumed, + consumed = consumed, available = left, source = NestedScrollSource.UserInput, ) @@ -482,10 +482,9 @@ private class NestedDraggableNode( val available = velocity - preConsumed val consumed = performFling(available) val left = available - consumed - return nestedScrollDispatcher.dispatchPostFling( - consumed = consumed + preConsumed, - available = left, - ) + val postConsumed = + nestedScrollDispatcher.dispatchPostFling(consumed = consumed, available = left) + return preConsumed + consumed + postConsumed } /* @@ -549,9 +548,10 @@ private class NestedDraggableNode( nestedScrollController == null && // TODO(b/388231324): Remove this. !lastEventWasScrollWheel && - draggable.shouldConsumeNestedScroll(sign) + draggable.shouldConsumeNestedScroll(sign) && + lastFirstDown != null ) { - val startedPosition = checkNotNull(lastFirstDown) { "lastFirstDown is not set" } + val startedPosition = checkNotNull(lastFirstDown) // TODO(b/382665591): Ensure that there is at least one pointer down. val pointersDownCount = pointersDown.size.coerceAtLeast(1) diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt index 5de0f1221f0f..19d28cc2d626 100644 --- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerType @@ -52,6 +53,7 @@ import androidx.compose.ui.test.performMouseInput import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown import androidx.compose.ui.test.swipeLeft +import androidx.compose.ui.test.swipeWithVelocity import androidx.compose.ui.unit.Velocity import com.google.common.truth.Truth.assertThat import kotlin.math.ceil @@ -773,6 +775,181 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3") } + @Test + fun nestedDragNotStartedWhenEnabledAfterDragStarted() { + val draggable = TestDraggable() + var enabled by mutableStateOf(false) + val touchSlop = + rule.setContentWithTouchSlop { + Box( + Modifier.fillMaxSize() + .nestedDraggable(draggable, orientation, enabled = enabled) + .scrollable(rememberScrollableState { 0f }, orientation) + ) + } + + rule.onRoot().performTouchInput { down(center) } + + enabled = true + rule.waitForIdle() + + rule.onRoot().performTouchInput { moveBy((touchSlop + 1f).toOffset()) } + + assertThat(draggable.onDragStartedCalled).isFalse() + } + + @Test + fun availableAndConsumedScrollDeltas() { + val totalScroll = 200f + val consumedByEffectPreScroll = 10f // 200f => 190f + val consumedByConnectionPreScroll = 20f // 190f => 170f + val consumedByScroll = 30f // 170f => 140f + val consumedByConnectionPostScroll = 40f // 140f => 100f + + // Available scroll values that we will check later. + var availableToEffectPreScroll = 0f + var availableToConnectionPreScroll = 0f + var availableToScroll = 0f + var availableToConnectionPostScroll = 0f + var availableToEffectPostScroll = 0f + + val effect = + TestOverscrollEffect( + orientation, + onPreScroll = { + availableToEffectPreScroll = it + consumedByEffectPreScroll + }, + onPostScroll = { + availableToEffectPostScroll = it + it + }, + ) + + val connection = + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + availableToConnectionPreScroll = available.toFloat() + return consumedByConnectionPreScroll.toOffset() + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + assertThat(consumed.toFloat()).isEqualTo(consumedByScroll) + availableToConnectionPostScroll = available.toFloat() + return consumedByConnectionPostScroll.toOffset() + } + } + + val draggable = + TestDraggable( + onDrag = { + availableToScroll = it + consumedByScroll + } + ) + + val touchSlop = + rule.setContentWithTouchSlop { + Box( + Modifier.fillMaxSize() + .nestedScroll(connection) + .nestedDraggable(draggable, orientation, effect) + ) + } + + rule.onRoot().performTouchInput { + down(center) + moveBy((touchSlop + totalScroll).toOffset()) + } + + assertThat(availableToEffectPreScroll).isEqualTo(200f) + assertThat(availableToConnectionPreScroll).isEqualTo(190f) + assertThat(availableToScroll).isEqualTo(170f) + assertThat(availableToConnectionPostScroll).isEqualTo(140f) + assertThat(availableToEffectPostScroll).isEqualTo(100f) + } + + @Test + fun availableAndConsumedVelocities() { + val totalVelocity = 200f + val consumedByEffectPreFling = 10f // 200f => 190f + val consumedByConnectionPreFling = 20f // 190f => 170f + val consumedByFling = 30f // 170f => 140f + val consumedByConnectionPostFling = 40f // 140f => 100f + + // Available velocities that we will check later. + var availableToEffectPreFling = 0f + var availableToConnectionPreFling = 0f + var availableToFling = 0f + var availableToConnectionPostFling = 0f + var availableToEffectPostFling = 0f + + val effect = + TestOverscrollEffect( + orientation, + onPreFling = { + availableToEffectPreFling = it + consumedByEffectPreFling + }, + onPostFling = { + availableToEffectPostFling = it + it + }, + onPostScroll = { 0f }, + ) + + val connection = + object : NestedScrollConnection { + override suspend fun onPreFling(available: Velocity): Velocity { + availableToConnectionPreFling = available.toFloat() + return consumedByConnectionPreFling.toVelocity() + } + + override suspend fun onPostFling( + consumed: Velocity, + available: Velocity, + ): Velocity { + assertThat(consumed.toFloat()).isEqualTo(consumedByFling) + availableToConnectionPostFling = available.toFloat() + return consumedByConnectionPostFling.toVelocity() + } + } + + val draggable = + TestDraggable( + onDragStopped = { velocity, _ -> + availableToFling = velocity + consumedByFling + }, + onDrag = { 0f }, + ) + + rule.setContent { + Box( + Modifier.fillMaxSize() + .nestedScroll(connection) + .nestedDraggable(draggable, orientation, effect) + ) + } + + rule.onRoot().performTouchInput { + when (orientation) { + Orientation.Horizontal -> swipeWithVelocity(topLeft, topRight, totalVelocity) + Orientation.Vertical -> swipeWithVelocity(topLeft, bottomLeft, totalVelocity) + } + } + + assertThat(availableToEffectPreFling).isWithin(1f).of(200f) + assertThat(availableToConnectionPreFling).isWithin(1f).of(190f) + assertThat(availableToFling).isWithin(1f).of(170f) + assertThat(availableToConnectionPostFling).isWithin(1f).of(140f) + assertThat(availableToEffectPostFling).isWithin(1f).of(100f) + } + private fun ComposeContentTestRule.setContentWithTouchSlop( content: @Composable () -> Unit ): Float { diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt index 8bf9c21639f4..0659f9198730 100644 --- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt @@ -24,6 +24,8 @@ import androidx.compose.ui.unit.Velocity class TestOverscrollEffect( override val orientation: Orientation, + private val onPreScroll: (Float) -> Float = { 0f }, + private val onPreFling: suspend (Float) -> Float = { 0f }, private val onPostFling: suspend (Float) -> Float = { it }, private val onPostScroll: (Float) -> Float, ) : OverscrollEffect, OrientationAware { @@ -36,19 +38,23 @@ class TestOverscrollEffect( source: NestedScrollSource, performScroll: (Offset) -> Offset, ): Offset { - val consumedByScroll = performScroll(delta) - val available = delta - consumedByScroll - val consumedByEffect = onPostScroll(available.toFloat()).toOffset() - return consumedByScroll + consumedByEffect + val consumedByPreScroll = onPreScroll(delta.toFloat()).toOffset() + val availableToScroll = delta - consumedByPreScroll + val consumedByScroll = performScroll(availableToScroll) + val availableToPostScroll = availableToScroll - consumedByScroll + val consumedByPostScroll = onPostScroll(availableToPostScroll.toFloat()).toOffset() + return consumedByPreScroll + consumedByScroll + consumedByPostScroll } override suspend fun applyToFling( velocity: Velocity, performFling: suspend (Velocity) -> Velocity, ) { - val consumedByFling = performFling(velocity) - val available = velocity - consumedByFling - onPostFling(available.toFloat()) + val consumedByPreFling = onPreFling(velocity.toFloat()).toVelocity() + val availableToFling = velocity - consumedByPreFling + val consumedByFling = performFling(availableToFling) + val availableToPostFling = availableToFling - consumedByFling + onPostFling(availableToPostFling.toFloat()) applyToFlingDone = true } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index 6d24fc16df23..aa8b4ae9000d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -48,8 +48,8 @@ import com.android.systemui.shade.ui.composable.Shade val SceneContainerTransitions = transitions { interruptionHandler = SceneContainerInterruptionHandler - // Overscroll progress starts linearly with some resistance (3f) and slowly approaches 0.2f - defaultSwipeSpec = spring(stiffness = 300f, dampingRatio = 0.8f, visibilityThreshold = 0.5f) + defaultMotionSpatialSpec = + spring(stiffness = 300f, dampingRatio = 0.8f, visibilityThreshold = 0.5f) // Scene transitions diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt index ce7a85b19fb4..e30e7d3ee34c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt @@ -30,7 +30,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.goneToSplitShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt index 1f7a7380bbc6..1a243ca48157 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt @@ -29,7 +29,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.lockscreenToSplitShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt index 24f285e81da2..a9af95bdcb8a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt @@ -27,7 +27,7 @@ fun TransitionBuilder.notificationsShadeToQuickSettingsShadeTransition( durationScale: Double = 1.0 ) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt index 3d62151baf2f..ddea5854d67e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt @@ -31,7 +31,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toNotificationsShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt index e78bc6afcc4f..e477a41ac608 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt @@ -28,7 +28,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toQuickSettingsShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt index bfae4897dc68..4db4934cf271 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt @@ -32,7 +32,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, 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 ff8efc28aa21..d50304d433f9 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 @@ -34,7 +34,7 @@ import com.android.internal.jank.Cuj.CujType /** The transitions configuration of a [SceneTransitionLayout]. */ class SceneTransitions internal constructor( - internal val defaultSwipeSpec: SpringSpec<Float>, + internal val defaultMotionSpatialSpec: SpringSpec<Float>, internal val transitionSpecs: List<TransitionSpecImpl>, internal val interruptionHandler: InterruptionHandler, ) { @@ -132,7 +132,7 @@ internal constructor( val Empty = SceneTransitions( - defaultSwipeSpec = DefaultSwipeSpec, + defaultMotionSpatialSpec = DefaultSwipeSpec, transitionSpecs = emptyList(), interruptionHandler = DefaultInterruptionHandler, ) @@ -194,9 +194,9 @@ internal interface TransformationSpec { * The [SpringSpec] used to animate the associated transition progress when the transition was * started by a swipe and is now animating back to a scene because the user lifted their finger. * - * If `null`, then the [SceneTransitions.defaultSwipeSpec] will be used. + * If `null`, then the [SceneTransitions.defaultMotionSpatialSpec] will be used. */ - val swipeSpec: SpringSpec<Float>? + val motionSpatialSpec: AnimationSpec<Float>? /** * The distance it takes for this transition to animate from 0% to 100% when it is driven by a @@ -213,7 +213,7 @@ internal interface TransformationSpec { internal val Empty = TransformationSpecImpl( progressSpec = snap(), - swipeSpec = null, + motionSpatialSpec = null, distance = null, transformationMatchers = emptyList(), ) @@ -246,7 +246,7 @@ internal class TransitionSpecImpl( val reverse = transformationSpec.invoke(transition) TransformationSpecImpl( progressSpec = reverse.progressSpec, - swipeSpec = reverse.swipeSpec, + motionSpatialSpec = reverse.motionSpatialSpec, distance = reverse.distance, transformationMatchers = reverse.transformationMatchers.map { @@ -276,7 +276,7 @@ internal class TransitionSpecImpl( */ internal class TransformationSpecImpl( override val progressSpec: AnimationSpec<Float>, - override val swipeSpec: SpringSpec<Float>?, + override val motionSpatialSpec: SpringSpec<Float>?, override val distance: UserActionDistance?, override val transformationMatchers: List<TransformationMatcher>, ) : TransformationSpec { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt index ba92f9bea07d..2bfa0199f30b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt @@ -365,10 +365,10 @@ internal class SwipeAnimation<T : ContentKey>( return 0f } - val swipeSpec = + val motionSpatialSpec = spec - ?: contentTransition.transformationSpec.swipeSpec - ?: layoutState.transitions.defaultSwipeSpec + ?: contentTransition.transformationSpec.motionSpatialSpec + ?: layoutState.transitions.defaultMotionSpatialSpec val velocityConsumed = CompletableDeferred<Float>() @@ -376,7 +376,7 @@ internal class SwipeAnimation<T : ContentKey>( val result = animatable.animateTo( targetValue = targetOffset, - animationSpec = swipeSpec, + animationSpec = motionSpatialSpec, initialVelocity = initialVelocity, ) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index 998054ef6c9e..776d553ee49c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -40,7 +40,7 @@ interface SceneTransitionsBuilder { * The default [AnimationSpec] used when after the user lifts their finger after starting a * swipe to transition, to animate back into one of the 2 scenes we are transitioning to. */ - var defaultSwipeSpec: SpringSpec<Float> + var defaultMotionSpatialSpec: SpringSpec<Float> /** * The [InterruptionHandler] used when transitions are interrupted. Defaults to @@ -145,9 +145,9 @@ interface TransitionBuilder : BaseTransitionBuilder { * The [SpringSpec] used to animate the associated transition progress when the transition was * started by a swipe and is now animating back to a scene because the user lifted their finger. * - * If `null`, then the [SceneTransitionsBuilder.defaultSwipeSpec] will be used. + * If `null`, then the [SceneTransitionsBuilder.defaultMotionSpatialSpec] will be used. */ - var swipeSpec: SpringSpec<Float>? + var motionSpatialSpec: SpringSpec<Float>? /** The CUJ associated to this transitions. */ @CujType var cuj: Int? diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index 7ca521513714..9a9b05eb3c1d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -41,11 +41,15 @@ import com.android.internal.jank.Cuj.CujType internal fun transitionsImpl(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions { val impl = SceneTransitionsBuilderImpl().apply(builder) - return SceneTransitions(impl.defaultSwipeSpec, impl.transitionSpecs, impl.interruptionHandler) + return SceneTransitions( + defaultMotionSpatialSpec = impl.defaultMotionSpatialSpec, + transitionSpecs = impl.transitionSpecs, + interruptionHandler = impl.interruptionHandler, + ) } private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { - override var defaultSwipeSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec + override var defaultMotionSpatialSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec override var interruptionHandler: InterruptionHandler = DefaultInterruptionHandler val transitionSpecs = mutableListOf<TransitionSpecImpl>() @@ -105,7 +109,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { val impl = TransitionBuilderImpl(transition).apply(builder) return TransformationSpecImpl( progressSpec = impl.spec, - swipeSpec = impl.swipeSpec, + motionSpatialSpec = impl.motionSpatialSpec, distance = impl.distance, transformationMatchers = impl.transformationMatchers, ) @@ -209,7 +213,7 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder { internal class TransitionBuilderImpl(override val transition: TransitionState.Transition) : BaseTransitionBuilderImpl(), TransitionBuilder { override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow) - override var swipeSpec: SpringSpec<Float>? = null + override var motionSpatialSpec: SpringSpec<Float>? = null override var distance: UserActionDistance? = null override var cuj: Int? = null private val durationMillis: Int by lazy { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt index 712af56ee1bc..097722665f8e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt @@ -390,10 +390,10 @@ sealed interface TransitionState { fun create(): Animatable<Float, AnimationVector1D> { val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold) layoutImpl.animationScope.launch { - val swipeSpec = layoutImpl.state.transitions.defaultSwipeSpec + val motionSpatialSpec = layoutImpl.state.transitions.defaultMotionSpatialSpec val progressSpec = spring( - stiffness = swipeSpec.stiffness, + stiffness = motionSpatialSpec.stiffness, dampingRatio = Spring.DampingRatioNoBouncy, visibilityThreshold = ProgressVisibilityThreshold, ) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt index 7c4dbf153013..00cd0ca564b1 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt @@ -104,7 +104,7 @@ fun TransitionBuilder.verticalContainerReveal( val alphaSpec = spring<Float>(stiffness = 1200f, dampingRatio = 0.99f) // The spring animating the progress when releasing the finger. - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, dampingRatio = Spring.DampingRatioNoBouncy, diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index dbac62ffb713..5a9edba26d13 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -137,13 +137,6 @@ class DraggableHandlerTest { var pointerInfoOwner: () -> PointersInfo = { pointersDown() } - fun nestedScrollConnection() = - NestedScrollHandlerImpl( - draggableHandler = draggableHandler, - pointersInfoOwner = { pointerInfoOwner() }, - ) - .connection - val velocityThreshold = draggableHandler.velocityThreshold fun down(fractionOfScreen: Float) = @@ -607,57 +600,6 @@ class DraggableHandlerTest { } @Test - fun nestedScrollUseFromSourceInfo() = runGestureTest { - // Start at scene C. - navigateToSceneC() - val nestedScroll = nestedScrollConnection() - - // Drag from the **top** of the screen - pointerInfoOwner = { pointersDown() } - assertIdle(currentScene = SceneC) - - nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) - assertTransition( - currentScene = SceneC, - fromScene = SceneC, - // userAction: Swipe.Up to SceneB - toScene = SceneB, - progress = 0.1f, - ) - - // Reset to SceneC - nestedScroll.preFling(Velocity.Zero) - advanceUntilIdle() - - // Drag from the **bottom** of the screen - pointerInfoOwner = { pointersDown(startedPosition = Offset(0f, SCREEN_SIZE)) } - assertIdle(currentScene = SceneC) - - nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) - assertTransition( - currentScene = SceneC, - fromScene = SceneC, - // userAction: Swipe.Up(fromSource = Edge.Bottom) to SceneA - toScene = SceneA, - progress = 0.1f, - ) - } - - @Test - fun ignoreMouseWheel() = runGestureTest { - // Start at scene C. - navigateToSceneC() - val nestedScroll = nestedScrollConnection() - - // Use mouse wheel - pointerInfoOwner = { PointersInfo.MouseWheel } - assertIdle(currentScene = SceneC) - - nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) - assertIdle(currentScene = SceneC) - } - - @Test fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest { // Swipe up from the middle to transition to scene B. val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) @@ -689,28 +631,10 @@ class DraggableHandlerTest { } @Test - fun scrollKeepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() = runGestureTest { - val nestedScroll = nestedScrollConnection() - - // Overscroll is disabled, it will scroll up to 100% - nestedScroll.scroll(available = upOffset(fractionOfScreen = 2f)) - assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f) - - // We need to maintain scroll priority even if the scene transition can no longer consume - // the scroll gesture. - nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) - assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f) - - // A scroll gesture in the opposite direction allows us to return to the previous scene. - nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.5f)) - assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.5f) - } - - @Test fun overscroll_releaseBetween0And100Percent_up() = runGestureTest { // Make scene B overscrollable. layoutState.transitions = transitions { - defaultSwipeSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy) + defaultMotionSpatialSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy) from(SceneA, to = SceneB) {} } @@ -739,7 +663,7 @@ class DraggableHandlerTest { fun overscroll_releaseBetween0And100Percent_down() = runGestureTest { // Make scene C overscrollable. layoutState.transitions = transitions { - defaultSwipeSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy) + defaultMotionSpatialSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy) from(SceneA, to = SceneC) {} } @@ -944,33 +868,4 @@ class DraggableHandlerTest { assertThat(layoutState.transitionState).hasCurrentScene(SceneA) assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB) } - - @Test - fun replaceOverlayNestedScroll() = runGestureTest { - layoutState.showOverlay(OverlayA, animationScope = testScope) - advanceUntilIdle() - - // Initial state. - assertThat(layoutState.transitionState).isIdle() - assertThat(layoutState.transitionState).hasCurrentScene(SceneA) - assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayA) - - // Swipe down to replace overlay A by overlay B. - - val nestedScroll = nestedScrollConnection() - nestedScroll.scroll(downOffset(0.1f)) - val transition = assertThat(layoutState.transitionState).isReplaceOverlayTransition() - assertThat(transition).hasCurrentScene(SceneA) - assertThat(transition).hasFromOverlay(OverlayA) - assertThat(transition).hasToOverlay(OverlayB) - assertThat(transition).hasCurrentOverlays(OverlayA) - assertThat(transition).hasProgress(0.1f) - - nestedScroll.preFling(Velocity(0f, velocityThreshold)) - advanceUntilIdle() - // Commit the gesture. The overlays are instantly swapped in the set of current overlays. - assertThat(layoutState.transitionState).isIdle() - assertThat(layoutState.transitionState).hasCurrentScene(SceneA) - assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB) - } } 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 53495be7b02a..005146997813 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 @@ -618,7 +618,7 @@ class ElementTest { fun layoutGetsCurrentTransitionStateFromComposition() { val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { @@ -1126,7 +1126,7 @@ class ElementTest { val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { spec = tween(duration, easing = LinearEasing) } @@ -1331,7 +1331,7 @@ class ElementTest { val fooSize = 100.dp val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { spec = tween(duration, easing = LinearEasing) } @@ -1439,7 +1439,7 @@ class ElementTest { @Test fun targetStateIsSetEvenWhenNotPlaced() { // Start directly at A => B but with progress < 0f to overscroll on A. - val state = rule.runOnUiThread { MutableSceneTransitionLayoutStateImpl(SceneA) } + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } lateinit var layoutImpl: SceneTransitionLayoutImpl val scope = @@ -1473,7 +1473,7 @@ class ElementTest { fun lastAlphaIsNotSetByOutdatedLayer() { val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { fade(TestElements.Foo) } }, ) @@ -1537,7 +1537,7 @@ class ElementTest { fun fadingElementsDontAppearInstantly() { val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { fade(TestElements.Foo) } }, ) @@ -1583,7 +1583,7 @@ class ElementTest { @Test fun lastPlacementValuesAreClearedOnNestedElements() { - val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } + val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) } @Composable fun ContentScope.NestedFooBar() { @@ -1658,7 +1658,7 @@ class ElementTest { fun currentTransitionSceneIsUsedToComputeElementValues() { val state = rule.runOnIdle { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneB, to = SceneC) { @@ -1709,7 +1709,7 @@ class ElementTest { @Test fun interruptionDeltasAreProperlyCleaned() { - val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } + val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) } @Composable fun ContentScope.Foo(offset: Dp) { @@ -1780,7 +1780,7 @@ class ElementTest { fun transparentElementIsNotImpactingInterruption() { val state = rule.runOnIdle { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { @@ -1856,7 +1856,7 @@ class ElementTest { @Test fun replacedTransitionDoesNotTriggerInterruption() { - val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } + val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) } @Composable fun ContentScope.Foo(modifier: Modifier = Modifier) { @@ -2027,7 +2027,7 @@ class ElementTest { ): SceneTransitionLayoutImpl { val state = rule.runOnIdle { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( from, transitions { from(from, to = to, preview = preview, builder = transition) }, ) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt index e80805a4e374..0355a30d5c73 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ScrollWheel +import androidx.compose.ui.test.TouchInjectionScope import androidx.compose.ui.test.assertPositionInRootIsEqualTo import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.createComposeRule @@ -55,6 +56,8 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestOverlays.OverlayA +import com.android.compose.animation.scene.TestOverlays.OverlayB import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC @@ -977,4 +980,164 @@ class SwipeToSceneTest { rule.waitForIdle() assertThat(state.transitionState).isSceneTransition() } + + @Test + fun nestedScroll_useFromSourceInfo() { + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene( + SceneA, + userActions = + mapOf(Swipe.Down to SceneB, Swipe.Down(fromSource = Edge.Top) to SceneC), + ) { + // Use a fullscreen nested scrollable to use the nested scroll connection. + Box( + Modifier.fillMaxSize() + .scrollable(rememberScrollableState { 0f }, Orientation.Vertical) + ) + } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + scene(SceneC) { Box(Modifier.fillMaxSize()) } + } + } + + // Swiping down from the middle of the screen leads to B. + rule.onRoot().performTouchInput { + down(center) + moveBy(Offset(0f, touchSlop + 1f)) + } + + var transition = assertThat(state.transitionState).isSceneTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneB) + + // Release finger and wait to settle back to A. + rule.onRoot().performTouchInput { up() } + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneA) + + // Swiping down from the top of the screen leads to B. + rule.onRoot().performTouchInput { + down(center.copy(y = 0f)) + moveBy(Offset(0f, touchSlop + 1f)) + } + + transition = assertThat(state.transitionState).isSceneTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneC) + } + + @Test + fun nestedScroll_ignoreMouseWheel() { + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { + // Use a fullscreen nested scrollable to use the nested scroll connection. + Box( + Modifier.fillMaxSize() + .scrollable(rememberScrollableState { 0f }, Orientation.Vertical) + ) + } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + } + } + + rule.onRoot().performMouseInput { + scroll(-touchSlop - 1f, scrollWheel = ScrollWheel.Vertical) + } + assertThat(state.transitionState).isIdle() + } + + @Test + fun nestedScroll_keepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() { + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { + // Use a fullscreen nested scrollable to use the nested scroll connection. + Box( + Modifier.fillMaxSize() + .scrollable(rememberScrollableState { 0f }, Orientation.Vertical) + ) + } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + } + } + + fun TouchInjectionScope.height() = bottom + fun TouchInjectionScope.halfHeight() = height() / 2f + + rule.onRoot().performTouchInput { + down(center.copy(y = 0f)) + moveBy(Offset(0f, touchSlop + halfHeight())) + } + val transition = assertThat(state.transitionState).isSceneTransition() + assertThat(transition).hasProgress(0.5f, tolerance = 0.01f) + + // The progress should never go above 100%. + rule.onRoot().performTouchInput { moveBy(Offset(0f, height())) } + assertThat(transition).hasProgress(1f, tolerance = 0.01f) + + // Because the overscroll effect of scene B is not attached, swiping in the opposite + // direction will directly decrease the progress. + rule.onRoot().performTouchInput { moveBy(Offset(0f, -halfHeight())) } + assertThat(transition).hasProgress(0.5f, tolerance = 0.01f) + } + + @Test + fun nestedScroll_replaceOverlay() { + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState(SceneA, initialOverlays = setOf(OverlayA)) + } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene(SceneA) { Box(Modifier.fillMaxSize()) } + overlay( + OverlayA, + mapOf(Swipe.Down to UserActionResult.ReplaceByOverlay(OverlayB)), + ) { + Box( + Modifier.fillMaxSize() + .scrollable(rememberScrollableState { 0f }, Orientation.Vertical) + ) + } + overlay(OverlayB) { Box(Modifier.fillMaxSize()) } + } + } + + // Swipe down 100% to replace A by B. + rule.onRoot().performTouchInput { + down(center.copy(y = 0f)) + moveBy(Offset(0f, touchSlop + bottom)) + } + + val transition = assertThat(state.transitionState).isReplaceOverlayTransition() + assertThat(transition).hasCurrentScene(SceneA) + assertThat(transition).hasFromOverlay(OverlayA) + assertThat(transition).hasToOverlay(OverlayB) + assertThat(transition).hasCurrentOverlays(OverlayA) + assertThat(transition).hasProgress(1f, tolerance = 0.01f) + + // Commit the gesture. The overlays are instantly swapped in the set of current overlays. + rule.onRoot().performTouchInput { up() } + assertThat(transition).hasCurrentScene(SceneA) + assertThat(transition).hasCurrentOverlays(OverlayB) + + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneA) + assertThat(state.transitionState).hasCurrentOverlays(OverlayB) + } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt index cb87fe849a81..aada4a50c89c 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt @@ -276,22 +276,22 @@ class TransitionDslTest { val defaultSpec = spring<Float>(stiffness = 1f) val specFromAToC = spring<Float>(stiffness = 2f) val transitions = transitions { - defaultSwipeSpec = defaultSpec + defaultMotionSpatialSpec = defaultSpec from(SceneA, to = SceneB) { // Default swipe spec. } - from(SceneA, to = SceneC) { swipeSpec = specFromAToC } + from(SceneA, to = SceneC) { motionSpatialSpec = specFromAToC } } - assertThat(transitions.defaultSwipeSpec).isSameInstanceAs(defaultSpec) + assertThat(transitions.defaultMotionSpatialSpec).isSameInstanceAs(defaultSpec) // A => B does not have a custom spec. assertThat( transitions .transitionSpec(from = SceneA, to = SceneB, key = null) .transformationSpec(aToB()) - .swipeSpec + .motionSpatialSpec ) .isNull() @@ -300,7 +300,7 @@ class TransitionDslTest { transitions .transitionSpec(from = SceneA, to = SceneC, key = null) .transformationSpec(transition(from = SceneA, to = SceneC)) - .swipeSpec + .motionSpatialSpec ) .isSameInstanceAs(specFromAToC) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java index 266591028efb..6edf94939010 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java @@ -65,7 +65,7 @@ public class DragToInteractAnimationControllerTest extends SysuiTestCase { @Before public void setUp() throws Exception { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); - final SecureSettings mockSecureSettings = TestUtils.mockSecureSettings(); + final SecureSettings mockSecureSettings = TestUtils.mockSecureSettings(mContext); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, mockSecureSettings, mHearingAidDeviceManager); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java index 241da5fbc444..15afd2559d9d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java @@ -71,7 +71,7 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { private AccessibilityManager mAccessibilityManager; @Mock private HearingAidDeviceManager mHearingAidDeviceManager; - private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(); + private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(mContext); private RecyclerView mStubListView; private MenuView mMenuView; private MenuViewLayer mMenuViewLayer; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java index 715c40a31632..56a97bb34172 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java @@ -89,7 +89,7 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { @Before public void setUp() throws Exception { final WindowManager windowManager = mContext.getSystemService(WindowManager.class); - final SecureSettings secureSettings = TestUtils.mockSecureSettings(); + final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, secureSettings, mHearingAidDeviceManager); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java index cb7c205742fc..5ff7bd063427 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java @@ -91,7 +91,7 @@ public class MenuViewTest extends SysuiTestCase { mSpyContext = spy(mContext); doNothing().when(mSpyContext).startActivity(any()); - final SecureSettings secureSettings = TestUtils.mockSecureSettings(); + final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, secureSettings, mHearingAidDeviceManager); final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java index 8399fa85bfb1..aafb21209468 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.ComponentName; +import android.content.Context; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; @@ -76,8 +77,10 @@ public class TestUtils { * Returns a mock secure settings configured to return information needed for tests. * Currently, this only includes button targets. */ - public static SecureSettings mockSecureSettings() { + public static SecureSettings mockSecureSettings(Context context) { SecureSettings secureSettings = mock(SecureSettings.class); + when(secureSettings.getRealUserHandle(UserHandle.USER_CURRENT)) + .thenReturn(context.getUserId()); final String targets = getShortcutTargets( Set.of(TEST_COMPONENT_A, TEST_COMPONENT_B)); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt index a8e390c25a4d..46d98f979655 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt @@ -23,11 +23,16 @@ import com.android.systemui.classifier.fakeFalsingManager import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest +import com.android.systemui.plugins.activityStarter import com.android.systemui.qs.panels.ui.viewmodel.toolbar.editModeButtonViewModelFactory import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @SmallTest @@ -36,6 +41,15 @@ class EditModeButtonViewModelTest : SysuiTestCase() { val underTest = kosmos.editModeButtonViewModelFactory.create() + @Before + fun setUp() { + with(kosmos) { + whenever(activityStarter.postQSRunnableDismissingKeyguard(any())).doAnswer { + (it.getArgument(0) as Runnable).run() + } + } + } + @Test fun falsingFalseTap_editModeDoesntStart() = kosmos.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt index a98d1a2ea4a5..d3ba3dceb4cf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt @@ -22,10 +22,13 @@ import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.scene.ui.view.mockShadeRootView import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository import com.android.systemui.testKosmos +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -41,6 +44,7 @@ import org.mockito.kotlin.whenever class ShadeDisplaysInteractorTest : SysuiTestCase() { val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val testScope = kosmos.testScope private val shadeRootview = kosmos.mockShadeRootView private val positionRepository = kosmos.fakeShadeDisplaysRepository private val shadeContext = kosmos.mockedWindowContext @@ -49,7 +53,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() { private val configuration = mock<Configuration>() private val display = mock<Display>() - private val underTest = kosmos.shadeDisplaysInteractor + private val underTest by lazy { kosmos.shadeDisplaysInteractor } @Before fun setup() { @@ -84,12 +88,14 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() { } @Test - fun start_shadeInWrongPosition_logsStartToLatencyTracker() { - whenever(display.displayId).thenReturn(0) - positionRepository.setDisplayId(1) + fun start_shadeInWrongPosition_logsStartToLatencyTracker() = + testScope.runTest { + whenever(display.displayId).thenReturn(0) + positionRepository.setDisplayId(1) - underTest.start() + underTest.start() + advanceUntilIdle() - verify(latencyTracker).onShadeDisplayChanging(eq(1)) - } + verify(latencyTracker).onShadeDisplayChanging(eq(1)) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt new file mode 100644 index 000000000000..58396e7cef82 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 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.shade.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl.NotificationElement +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl.QSElement +import com.android.systemui.shade.shadeTestUtil +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +@EnableSceneContainer +class ShadeExpandedStateInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val testScope = kosmos.testScope + private val shadeTestUtil by lazy { kosmos.shadeTestUtil } + + private val underTest: ShadeExpandedStateInteractor by lazy { + kosmos.shadeExpandedStateInteractor + } + + @Test + fun expandedElement_qsExpanded_returnsQSElement() = + testScope.runTest { + shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 0f, qsExpansion = 1f) + val currentlyExpandedElement = underTest.currentlyExpandedElement + + val element = currentlyExpandedElement.value + + assertThat(element).isInstanceOf(QSElement::class.java) + } + + @Test + fun expandedElement_shadeExpanded_returnsShade() = + testScope.runTest { + shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 1f, qsExpansion = 0f) + + val element = underTest.currentlyExpandedElement.value + + assertThat(element).isInstanceOf(NotificationElement::class.java) + } + + @Test + fun expandedElement_noneExpanded_returnsNull() = + testScope.runTest { + shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 0f, qsExpansion = 0f) + + val element = underTest.currentlyExpandedElement.value + + assertThat(element).isNull() + } +} diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index 58f2d3ccc6a8..67f620f6fc54 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -19,6 +19,7 @@ android:id="@+id/volume_dialog_root" android:layout_width="match_parent" android:layout_height="match_parent" + android:alpha="0" android:clipChildren="false" app:layoutDescription="@xml/volume_dialog_scene"> diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java index 04afd8693e04..caf043a1b1be 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java @@ -22,6 +22,8 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; +import android.annotation.IntDef; +import android.annotation.Nullable; import android.annotation.UiContext; import android.content.ComponentCallbacks; import android.content.Context; @@ -44,7 +46,8 @@ import android.view.SurfaceControlViewHost; import android.view.View; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; -import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import androidx.annotation.NonNull; @@ -57,12 +60,16 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.res.R; import com.android.systemui.util.leak.RotationUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.concurrent.Executor; import java.util.function.Supplier; public class FullscreenMagnificationController implements ComponentCallbacks { - private static final String TAG = "FullscreenMagnificationController"; + private static final String TAG = "FullscreenMagController"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private final Context mContext; private final AccessibilityManager mAccessibilityManager; private final WindowManager mWindowManager; @@ -77,12 +84,14 @@ public class FullscreenMagnificationController implements ComponentCallbacks { private int mBorderStoke; private final int mDisplayId; private static final Region sEmptyRegion = new Region(); - private ValueAnimator mShowHideBorderAnimator; + @VisibleForTesting + @Nullable + ValueAnimator mShowHideBorderAnimator; private Handler mHandler; private Executor mExecutor; - private boolean mFullscreenMagnificationActivated = false; private final Configuration mConfiguration; - private final Runnable mShowBorderRunnable = this::showBorderWithNullCheck; + private final Runnable mHideBorderImmediatelyRunnable = this::hideBorderImmediately; + private final Runnable mShowBorderRunnable = this::showBorder; private int mRotation; private final IRotationWatcher mRotationWatcher = new IRotationWatcher.Stub() { @Override @@ -95,6 +104,21 @@ public class FullscreenMagnificationController implements ComponentCallbacks { private final DisplayManager.DisplayListener mDisplayListener; private String mCurrentDisplayUniqueId; + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + DISABLED, + DISABLING, + ENABLING, + ENABLED + }) + @interface FullscreenMagnificationActivationState {} + private static final int DISABLED = 0; + private static final int DISABLING = 1; + private static final int ENABLING = 2; + private static final int ENABLED = 3; + @FullscreenMagnificationActivationState + private int mActivationState = DISABLED; + public FullscreenMagnificationController( @UiContext Context context, @Main Handler handler, @@ -106,7 +130,7 @@ public class FullscreenMagnificationController implements ComponentCallbacks { Supplier<SurfaceControlViewHost> scvhSupplier) { this(context, handler, executor, displayManager, accessibilityManager, windowManager, iWindowManager, scvhSupplier, - new SurfaceControl.Transaction(), null); + new SurfaceControl.Transaction()); } @VisibleForTesting @@ -119,8 +143,7 @@ public class FullscreenMagnificationController implements ComponentCallbacks { WindowManager windowManager, IWindowManager iWindowManager, Supplier<SurfaceControlViewHost> scvhSupplier, - SurfaceControl.Transaction transaction, - ValueAnimator valueAnimator) { + SurfaceControl.Transaction transaction) { mContext = context; mHandler = handler; mExecutor = executor; @@ -135,18 +158,6 @@ public class FullscreenMagnificationController implements ComponentCallbacks { mConfiguration = new Configuration(context.getResources().getConfiguration()); mLongAnimationTimeMs = mContext.getResources().getInteger( com.android.internal.R.integer.config_longAnimTime); - mShowHideBorderAnimator = (valueAnimator == null) - ? createNullTargetObjectAnimator() : valueAnimator; - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { - if (isReverse) { - // The animation was played in reverse, which means we are hiding the border. - // We would like to perform clean up after the border is fully hidden. - cleanUpBorder(); - } - } - }); mCurrentDisplayUniqueId = mContext.getDisplayNoVerify().getUniqueId(); mDisplayManager = displayManager; mDisplayListener = new DisplayManager.DisplayListener() { @@ -167,20 +178,51 @@ public class FullscreenMagnificationController implements ComponentCallbacks { // Same unique ID means the physical display doesn't change. Early return. return; } - mCurrentDisplayUniqueId = uniqueId; - applyCornerRadiusToBorder(); + mHandler.post(FullscreenMagnificationController.this::applyCornerRadiusToBorder); } }; } - private ValueAnimator createNullTargetObjectAnimator() { + @VisibleForTesting + @UiThread + ValueAnimator createShowTargetAnimator(@NonNull View target) { + if (mShowHideBorderAnimator != null) { + mShowHideBorderAnimator.cancel(); + } + final ValueAnimator valueAnimator = - ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f); - Interpolator interpolator = new AccelerateDecelerateInterpolator(); + ObjectAnimator.ofFloat(target, View.ALPHA, 0f, 1f); + Interpolator interpolator = new AccelerateInterpolator(); valueAnimator.setInterpolator(interpolator); valueAnimator.setDuration(mLongAnimationTimeMs); + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@NonNull Animator animation) { + mHandler.post(() -> setState(ENABLED)); + }}); + return valueAnimator; + } + + @VisibleForTesting + @UiThread + ValueAnimator createHideTargetAnimator(@NonNull View target) { + if (mShowHideBorderAnimator != null) { + mShowHideBorderAnimator.cancel(); + } + + final ValueAnimator valueAnimator = + ObjectAnimator.ofFloat(target, View.ALPHA, 1f, 0f); + Interpolator interpolator = new DecelerateInterpolator(); + + valueAnimator.setInterpolator(interpolator); + valueAnimator.setDuration(mLongAnimationTimeMs); + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@NonNull Animator animation) { + mHandler.post(() -> cleanUpBorder()); + }}); return valueAnimator; } @@ -190,14 +232,10 @@ public class FullscreenMagnificationController implements ComponentCallbacks { */ @UiThread public void onFullscreenMagnificationActivationChanged(boolean activated) { - final boolean changed = (mFullscreenMagnificationActivated != activated); - if (changed) { - mFullscreenMagnificationActivated = activated; - if (activated) { - createFullscreenMagnificationBorder(); - } else { - removeFullscreenMagnificationBorder(); - } + if (activated) { + createFullscreenMagnificationBorder(); + } else { + removeFullscreenMagnificationBorder(); } } @@ -207,16 +245,21 @@ public class FullscreenMagnificationController implements ComponentCallbacks { */ @UiThread private void removeFullscreenMagnificationBorder() { - if (mHandler.hasCallbacks(mShowBorderRunnable)) { - mHandler.removeCallbacks(mShowBorderRunnable); + int state = getState(); + if (state == DISABLING || state == DISABLED) { + // If there is an ongoing disable process or it is already disabled, return + return; } - mContext.unregisterComponentCallbacks(this); - - - mShowHideBorderAnimator.reverse(); + setState(DISABLING); + mShowHideBorderAnimator = createHideTargetAnimator(mFullscreenBorder); + mShowHideBorderAnimator.start(); } - private void cleanUpBorder() { + @VisibleForTesting + @UiThread + void cleanUpBorder() { + mContext.unregisterComponentCallbacks(this); + if (Flags.updateCornerRadiusOnDisplayChanged()) { mDisplayManager.unregisterDisplayListener(mDisplayListener); } @@ -227,6 +270,12 @@ public class FullscreenMagnificationController implements ComponentCallbacks { } if (mFullscreenBorder != null) { + if (mHandler.hasCallbacks(mHideBorderImmediatelyRunnable)) { + mHandler.removeCallbacks(mHideBorderImmediatelyRunnable); + } + if (mHandler.hasCallbacks(mShowBorderRunnable)) { + mHandler.removeCallbacks(mShowBorderRunnable); + } mFullscreenBorder = null; try { mIWindowManager.removeRotationWatcher(mRotationWatcher); @@ -234,6 +283,7 @@ public class FullscreenMagnificationController implements ComponentCallbacks { Log.w(TAG, "Failed to remove rotation watcher", e); } } + setState(DISABLED); } /** @@ -242,44 +292,47 @@ public class FullscreenMagnificationController implements ComponentCallbacks { */ @UiThread private void createFullscreenMagnificationBorder() { + int state = getState(); + if (state == ENABLING || state == ENABLED) { + // If there is an ongoing enable process or it is already enabled, return + return; + } + if (mShowHideBorderAnimator != null) { + mShowHideBorderAnimator.cancel(); + } + setState(ENABLING); + onConfigurationChanged(mContext.getResources().getConfiguration()); mContext.registerComponentCallbacks(this); if (mSurfaceControlViewHost == null) { - // Create the view only if it does not exist yet. If we are trying to enable fullscreen - // magnification before it was fully disabled, we use the previous view instead of - // creating a new one. + // Create the view only if it does not exist yet. If we are trying to enable + // fullscreen magnification before it was fully disabled, we use the previous view + // instead of creating a new one. mFullscreenBorder = LayoutInflater.from(mContext) .inflate(R.layout.fullscreen_magnification_border, null); - // Set the initial border view alpha manually so we won't show the border accidentally - // after we apply show() to the SurfaceControl and before the animation starts to run. + // Set the initial border view alpha manually so we won't show the border + // accidentally after we apply show() to the SurfaceControl and before the + // animation starts to run. mFullscreenBorder.setAlpha(0f); - mShowHideBorderAnimator.setTarget(mFullscreenBorder); mSurfaceControlViewHost = mScvhSupplier.get(); mSurfaceControlViewHost.setView(mFullscreenBorder, getBorderLayoutParams()); - mBorderSurfaceControl = mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(); + mBorderSurfaceControl = + mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(); try { mIWindowManager.watchRotation(mRotationWatcher, Display.DEFAULT_DISPLAY); } catch (Exception e) { Log.w(TAG, "Failed to register rotation watcher", e); } if (Flags.updateCornerRadiusOnDisplayChanged()) { - mHandler.post(this::applyCornerRadiusToBorder); + applyCornerRadiusToBorder(); } } mTransaction .addTransactionCommittedListener( mExecutor, - () -> { - if (mShowHideBorderAnimator.isRunning()) { - // Since the method is only called when there is an activation - // status change, the running animator is hiding the border. - mShowHideBorderAnimator.reverse(); - } else { - mShowHideBorderAnimator.start(); - } - }) + this::showBorder) .setPosition(mBorderSurfaceControl, -mBorderOffset, -mBorderOffset) .setLayer(mBorderSurfaceControl, Integer.MAX_VALUE) .show(mBorderSurfaceControl) @@ -380,19 +433,25 @@ public class FullscreenMagnificationController implements ComponentCallbacks { mHandler.removeCallbacks(mShowBorderRunnable); } - // We hide the border immediately as early as possible to beat the redrawing of window - // in response to the orientation change so users won't see a weird shape border. - mHandler.postAtFrontOfQueue(() -> { - mFullscreenBorder.setAlpha(0f); - }); - + // We hide the border immediately as early as possible to beat the redrawing of + // window in response to the orientation change so users won't see a weird shape + // border. + mHandler.postAtFrontOfQueue(mHideBorderImmediatelyRunnable); mHandler.postDelayed(mShowBorderRunnable, mLongAnimationTimeMs); } - private void showBorderWithNullCheck() { + @UiThread + private void hideBorderImmediately() { if (mShowHideBorderAnimator != null) { - mShowHideBorderAnimator.start(); + mShowHideBorderAnimator.cancel(); } + mFullscreenBorder.setAlpha(0f); + } + + @UiThread + private void showBorder() { + mShowHideBorderAnimator = createShowTargetAnimator(mFullscreenBorder); + mShowHideBorderAnimator.start(); } private void updateDimensions() { @@ -404,7 +463,9 @@ public class FullscreenMagnificationController implements ComponentCallbacks { R.dimen.magnifier_border_width_fullscreen_with_offset); } - private void applyCornerRadiusToBorder() { + @UiThread + @VisibleForTesting + void applyCornerRadiusToBorder() { if (!isActivated()) { return; } @@ -422,6 +483,20 @@ public class FullscreenMagnificationController implements ComponentCallbacks { backgroundDrawable.setCornerRadius(cornerRadius); } + @UiThread + private void setState(@FullscreenMagnificationActivationState int state) { + if (DEBUG) { + Log.d(TAG, "setState from " + mActivationState + " to " + state); + } + mActivationState = state; + } + + @VisibleForTesting + @UiThread + int getState() { + return mActivationState; + } + @Override public void onLowMemory() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java index 5f0acfa644dc..67aa4ff577b8 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java @@ -23,7 +23,6 @@ import android.annotation.Nullable; import android.content.Context; import android.hardware.display.DisplayManager; import android.os.Handler; -import android.os.UserHandle; import android.text.TextUtils; import android.view.Display; import android.view.WindowManager; @@ -58,7 +57,7 @@ public class AccessibilityFloatingMenuController implements private final AccessibilityButtonTargetsObserver mAccessibilityButtonTargetsObserver; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private Context mContext; + private final Context mContext; private final WindowManager mWindowManager; private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private final DisplayManager mDisplayManager; @@ -226,7 +225,6 @@ public class AccessibilityFloatingMenuController implements @Override public void onUserInitializationComplete(int userId) { mIsUserInInitialization = false; - mContext = mContext.createContextAsUser(UserHandle.of(userId), /* flags= */ 0); mBtnMode = mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode(); mBtnTargets = mAccessibilityButtonTargetsObserver.getCurrentAccessibilityButtonTargets(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java index 121b51f768e7..a1cb0367421b 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java @@ -79,6 +79,8 @@ class MenuInfoRepository { private static final int DEFAULT_MIGRATION_TOOLTIP_VALUE_PROMPT = MigrationPrompt.DISABLED; private final Context mContext; + // Pref always get the userId from the context to store SharedPreferences for the correct user + private final Context mCurrentUserContext; private final Configuration mConfiguration; private final AccessibilityManager mAccessibilityManager; private final AccessibilityManager.AccessibilityServicesStateChangeListener @@ -157,6 +159,9 @@ class MenuInfoRepository { OnContentsChanged settingsContentsChanged, SecureSettings secureSettings, @Nullable HearingAidDeviceManager hearingAidDeviceManager) { mContext = context; + final int currentUserId = secureSettings.getRealUserHandle(UserHandle.USER_CURRENT); + mCurrentUserContext = context.createContextAsUser( + UserHandle.of(currentUserId), /* flags= */ 0); mAccessibilityManager = accessibilityManager; mConfiguration = new Configuration(context.getResources().getConfiguration()); mSettingsContentsCallback = settingsContentsChanged; @@ -168,12 +173,13 @@ class MenuInfoRepository { void loadMenuMoveToTucked(OnInfoReady<Boolean> callback) { callback.onReady( - Prefs.getBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, + Prefs.getBoolean( + mCurrentUserContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, DEFAULT_MOVE_TO_TUCKED_VALUE)); } void loadDockTooltipVisibility(OnInfoReady<Boolean> callback) { - callback.onReady(Prefs.getBoolean(mContext, + callback.onReady(Prefs.getBoolean(mCurrentUserContext, Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, DEFAULT_HAS_SEEN_DOCK_TOOLTIP_VALUE)); } @@ -215,19 +221,19 @@ class MenuInfoRepository { } void updateMoveToTucked(boolean isMoveToTucked) { - Prefs.putBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, + Prefs.putBoolean(mCurrentUserContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, isMoveToTucked); } void updateMenuSavingPosition(Position percentagePosition) { mPercentagePosition = percentagePosition; - Prefs.putString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, + Prefs.putString(mCurrentUserContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, percentagePosition.toString()); } void updateDockTooltipVisibility(boolean hasSeen) { - Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, - hasSeen); + Prefs.putBoolean(mCurrentUserContext, + Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, hasSeen); } void updateMigrationTooltipVisibility(boolean visible) { @@ -243,7 +249,7 @@ class MenuInfoRepository { } private Position getStartPosition() { - final String absolutePositionString = Prefs.getString(mContext, + final String absolutePositionString = Prefs.getString(mCurrentUserContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null); final float defaultPositionXPercent = diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java index 184518ac35eb..e7470a34a065 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java @@ -17,6 +17,7 @@ package com.android.systemui.accessibility.floatingmenu; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; +import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; import android.content.Context; import android.graphics.PixelFormat; @@ -90,7 +91,8 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu { WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); params.receiveInsetsIgnoringZOrder = true; - params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; + params.privateFlags |= + PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION | SYSTEM_FLAG_SHOW_FOR_ALL_USERS; params.windowAnimations = android.R.style.Animation_Translucent; // Insets are configured to allow the menu to display over navigation and system bars. params.setFitInsetsTypes(0); diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt index f60621882ac0..59990ea22c2f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.panels.ui.viewmodel.toolbar import com.android.systemui.classifier.domain.interactor.FalsingInteractor +import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel import dagger.assisted.AssistedFactory @@ -27,11 +28,12 @@ class EditModeButtonViewModel constructor( private val editModeViewModel: EditModeViewModel, private val falsingInteractor: FalsingInteractor, + private val activityStarter: ActivityStarter, ) { fun onButtonClick() { if (!falsingInteractor.isFalseTap(FalsingManager.LOW_PENALTY)) { - editModeViewModel.startEditing() + activityStarter.postQSRunnableDismissingKeyguard { editModeViewModel.startEditing() } } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index 42d83637ec1a..a48d4d4d3b5f 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -316,7 +316,7 @@ internal constructor( val callback = it.callback.get() if (callback != null) { it.executor.execute { - traceSection({ "$callback" }) { action(callback) { latch.countDown() } } + traceSection({ "UserTrackerImpl::$callback" }) { action(callback) { latch.countDown() } } } } else { latch.countDown() diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index c4306d3f7530..19bf4c0bab81 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -3959,7 +3959,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } final boolean isTrackpadThreeFingerSwipe = isTrackpadThreeFingerSwipe(event); - if (com.android.systemui.Flags.disableShadeExpandsOnTrackpadTwoFingerSwipe() + if (com.android.systemui.Flags.disableShadeTrackpadTwoFingerSwipe() && !isTrackpadThreeFingerSwipe && isTwoFingerSwipeTrackpadEvent(event) && !isPanelExpanded()) { if (isDown) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt index 63e8ba8f65cd..747642097327 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt @@ -37,12 +37,13 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository -import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor -import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.data.repository.ShadeDisplaysRepositoryImpl import com.android.systemui.shade.display.ShadeDisplayPolicyModule +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl import com.android.systemui.shade.domain.interactor.ShadeDisplaysInteractor +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.statusbar.phone.ConfigurationControllerImpl import com.android.systemui.statusbar.phone.ConfigurationForwarder @@ -276,6 +277,8 @@ object ShadeDisplayAwareModule { @Module internal interface OptionalShadeDisplayAwareBindings { @BindsOptionalOf fun bindOptionalOfWindowRootView(): WindowRootView + + @BindsOptionalOf fun bindOptionalOShadeExpandedStateInteractor(): ShadeExpandedStateInteractor } /** diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index 2348a110eb3a..b9df9f868dc3 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -35,6 +35,8 @@ import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorLega import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorSceneContainerImpl import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractorImpl +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl @@ -176,4 +178,10 @@ abstract class ShadeModule { @Binds @SysUISingleton abstract fun bindShadeModeInteractor(impl: ShadeModeInteractorImpl): ShadeModeInteractor + + @Binds + @SysUISingleton + abstract fun bindShadeExpandedStateInteractor( + impl: ShadeExpandedStateInteractorImpl + ): ShadeExpandedStateInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt index 11805992fd6a..9a9fc467c53f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt @@ -27,7 +27,7 @@ import com.android.app.tracing.coroutines.TrackTracer * them across various threads' logs. */ object ShadeTraceLogger { - private val t = TrackTracer(trackName = "ShadeTraceLogger", trackGroup = "shade") + val t = TrackTracer(trackName = "ShadeTraceLogger", trackGroup = "shade") @JvmStatic fun logOnMovedToDisplay(displayId: Int, config: Configuration) { @@ -44,8 +44,11 @@ object ShadeTraceLogger { t.instant { "moveShadeWindowTo(displayId=$displayId)" } } - @JvmStatic - fun traceReparenting(r: () -> Unit) { + suspend fun traceReparenting(r: suspend () -> Unit) { t.traceAsync({ "reparenting" }) { r() } } + + inline fun traceWaitForExpansion(expansion: Float, r: () -> Unit) { + t.traceAsync({ "waiting for shade expansion to match $expansion" }) { r() } + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt new file mode 100644 index 000000000000..eab00166c8ef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 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.shade.domain.interactor + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** Fake [ShadeExpandedStateInteractor] for tests. */ +class FakeShadeExpandedStateInteractor : ShadeExpandedStateInteractor { + + private val mutableExpandedElement = + MutableStateFlow<ShadeExpandedStateInteractor.ShadeElement?>(null) + override val currentlyExpandedElement: StateFlow<ShadeExpandedStateInteractor.ShadeElement?> + get() = mutableExpandedElement + + fun setState(state: ShadeExpandedStateInteractor.ShadeElement?) { + mutableExpandedElement.value = state + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt index be561b178136..691a383cb338 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt @@ -32,6 +32,7 @@ import com.android.systemui.shade.ShadeTraceLogger.traceReparenting import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.window.flags.Flags +import java.util.Optional import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope @@ -47,8 +48,19 @@ constructor( @Background private val bgScope: CoroutineScope, @Main private val mainThreadContext: CoroutineContext, private val shadeDisplayChangeLatencyTracker: ShadeDisplayChangeLatencyTracker, + shadeExpandedInteractor: Optional<ShadeExpandedStateInteractor>, ) : CoreStartable { + private val shadeExpandedInteractor = + shadeExpandedInteractor.orElse(null) + ?: error( + """ + ShadeExpandedStateInteractor must be provided for ShadeDisplaysInteractor to work. + If it is not, it means this is being instantiated in a SystemUI variant that shouldn't. + """ + .trimIndent() + ) + override fun start() { ShadeWindowGoesAround.isUnexpectedlyInLegacyMode() bgScope.launchTraced(TAG) { @@ -78,9 +90,12 @@ constructor( withContext(mainThreadContext) { traceReparenting { shadeDisplayChangeLatencyTracker.onShadeDisplayChanging(destinationId) + val expandedElement = shadeExpandedInteractor.currentlyExpandedElement.value + expandedElement?.collapse(reason = "Shade window move") reparentToDisplayId(id = destinationId) + expandedElement?.expand(reason = "Shade window move") + checkContextDisplayMatchesExpected(destinationId) } - checkContextDisplayMatchesExpected(destinationId) } } catch (e: IllegalStateException) { Log.e( diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt new file mode 100644 index 000000000000..dd3abeec5a72 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2025 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.shade.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.shade.ShadeTraceLogger.traceWaitForExpansion +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor.ShadeElement +import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.util.kotlin.Utils.Companion.combineState +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout + +/** + * Wrapper around [ShadeInteractor] to facilitate expansion and collapse of Notifications and quick + * settings. + * + * Specifially created to simplify [ShadeDisplaysInteractor] logic. + * + * NOTE: with [SceneContainerFlag] or [DualShade] disabled, [currentlyExpandedElement] will always + * return null! + */ +interface ShadeExpandedStateInteractor { + /** Returns the expanded [ShadeElement]. If none is, returns null. */ + val currentlyExpandedElement: StateFlow<ShadeElement?> + + /** An element from the shade window that can be expanded or collapsed. */ + abstract class ShadeElement { + /** Expands the shade element, returning when the expansion is done */ + abstract suspend fun expand(reason: String) + + /** Collapses the shade element, returning when the collapse is done. */ + abstract suspend fun collapse(reason: String) + } +} + +@SysUISingleton +class ShadeExpandedStateInteractorImpl +@Inject +constructor( + private val shadeInteractor: ShadeInteractor, + @Background private val bgScope: CoroutineScope, +) : ShadeExpandedStateInteractor { + + private val notificationElement = NotificationElement() + private val qsElement = QSElement() + + override val currentlyExpandedElement: StateFlow<ShadeElement?> = + if (SceneContainerFlag.isEnabled) { + combineState( + shadeInteractor.isShadeAnyExpanded, + shadeInteractor.isQsExpanded, + bgScope, + SharingStarted.Eagerly, + ) { isShadeAnyExpanded, isQsExpanded -> + when { + isShadeAnyExpanded -> notificationElement + isQsExpanded -> qsElement + else -> null + } + } + } else { + MutableStateFlow(null) + } + + inner class NotificationElement : ShadeElement() { + override suspend fun expand(reason: String) { + shadeInteractor.expandNotificationsShade(reason) + shadeInteractor.shadeExpansion.waitUntil(1f) + } + + override suspend fun collapse(reason: String) { + shadeInteractor.collapseNotificationsShade(reason) + shadeInteractor.shadeExpansion.waitUntil(0f) + } + } + + inner class QSElement : ShadeElement() { + override suspend fun expand(reason: String) { + shadeInteractor.expandQuickSettingsShade(reason) + shadeInteractor.qsExpansion.waitUntil(1f) + } + + override suspend fun collapse(reason: String) { + shadeInteractor.collapseQuickSettingsShade(reason) + shadeInteractor.qsExpansion.waitUntil(0f) + } + } + + private suspend fun StateFlow<Float>.waitUntil(f: Float) { + // it's important to not do this in the main thread otherwise it will block any rendering. + withContext(bgScope.coroutineContext) { + withTimeout(1.seconds) { traceWaitForExpansion(expansion = f) { first { it == f } } } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt index d9a2e956cc86..a88b127ae157 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt @@ -17,10 +17,14 @@ package com.android.systemui.util.kotlin import android.content.Context +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn class Utils { companion object { @@ -32,6 +36,7 @@ class Utils { fun <A, B, C, D> toQuad(a: A, bcd: Triple<B, C, D>) = Quad(a, bcd.first, bcd.second, bcd.third) + fun <A, B, C, D> toQuad(abc: Triple<A, B, C>, d: D) = Quad(abc.first, abc.second, abc.third, d) @@ -51,7 +56,7 @@ class Utils { bcdefg.third, bcdefg.fourth, bcdefg.fifth, - bcdefg.sixth + bcdefg.sixth, ) /** @@ -81,7 +86,7 @@ class Utils { fun <A, B, C, D> Flow<A>.sample( b: Flow<B>, c: Flow<C>, - d: Flow<D> + d: Flow<D>, ): Flow<Quad<A, B, C, D>> { return this.sample(combine(b, c, d, ::Triple), ::toQuad) } @@ -134,6 +139,20 @@ class Utils { ): Flow<Septuple<A, B, C, D, E, F, G>> { return this.sample(combine(b, c, d, e, f, g, ::Sextuple), ::toSeptuple) } + + /** + * Combines 2 state flows, applying [transform] between the initial values to set the + * initial value of the resulting StateFlow. + */ + fun <A, B, R> combineState( + f1: StateFlow<A>, + f2: StateFlow<B>, + scope: CoroutineScope, + sharingStarted: SharingStarted, + transform: (A, B) -> R, + ): StateFlow<R> = + combine(f1, f2) { a, b -> transform(a, b) } + .stateIn(scope, sharingStarted, transform(f1.value, f2.value)) } } @@ -144,7 +163,7 @@ data class Quint<A, B, C, D, E>( val second: B, val third: C, val fourth: D, - val fifth: E + val fifth: E, ) data class Sextuple<A, B, C, D, E, F>( diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt index e8d19dd5e0e4..96630ca36b97 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt @@ -51,6 +51,8 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch private const val CLOSE_DRAWER_DELAY = 300L +// Ensure roundness and color of button is updated when progress is changed by a minimum fraction. +private const val BUTTON_MIN_VISIBLE_CHANGE = 0.05F @OptIn(ExperimentalCoroutinesApi::class) @VolumeDialogScope @@ -58,12 +60,12 @@ class VolumeDialogRingerViewBinder @Inject constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) { private val roundnessSpringForce = - SpringForce(0F).apply { + SpringForce(1F).apply { stiffness = 800F dampingRatio = 0.6F } private val colorSpringForce = - SpringForce(0F).apply { + SpringForce(1F).apply { stiffness = 3800F dampingRatio = 1F } @@ -257,30 +259,35 @@ constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) { // We only need to execute on roundness animation end and volume dialog background // progress update once because these changes should be applied once on volume dialog // background and ringer drawer views. - val selectedCornerRadius = (selectedButton.background as GradientDrawable).cornerRadius - if (selectedCornerRadius.toInt() != selectedButtonUiModel.cornerRadius) { - selectedButton.animateTo( - selectedButtonUiModel, - if (uiModel.currentButtonIndex == count - 1) { - onProgressChanged - } else { - { _, _ -> } - }, - ) - } - val unselectedCornerRadius = - (unselectedButton.background as GradientDrawable).cornerRadius - if (unselectedCornerRadius.toInt() != unselectedButtonUiModel.cornerRadius) { - unselectedButton.animateTo( - unselectedButtonUiModel, - if (previousIndex == count - 1) { - onProgressChanged - } else { - { _, _ -> } - }, - ) - } coroutineScope { + val selectedCornerRadius = + (selectedButton.background as GradientDrawable).cornerRadius + if (selectedCornerRadius.toInt() != selectedButtonUiModel.cornerRadius) { + launch { + selectedButton.animateTo( + selectedButtonUiModel, + if (uiModel.currentButtonIndex == count - 1) { + onProgressChanged + } else { + { _, _ -> } + }, + ) + } + } + val unselectedCornerRadius = + (unselectedButton.background as GradientDrawable).cornerRadius + if (unselectedCornerRadius.toInt() != unselectedButtonUiModel.cornerRadius) { + launch { + unselectedButton.animateTo( + unselectedButtonUiModel, + if (previousIndex == count - 1) { + onProgressChanged + } else { + { _, _ -> } + }, + ) + } + } launch { delay(CLOSE_DRAWER_DELAY) bindButtons(viewModel, uiModel, onAnimationEnd, isAnimated = true) @@ -383,11 +390,14 @@ constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) { onProgressChanged: (Float, Boolean) -> Unit = { _, _ -> }, ) { val roundnessAnimation = - SpringAnimation(FloatValueHolder(0F)).setSpring(roundnessSpringForce) - val colorAnimation = SpringAnimation(FloatValueHolder(0F)).setSpring(colorSpringForce) + SpringAnimation(FloatValueHolder(0F), 1F).setSpring(roundnessSpringForce) + val colorAnimation = SpringAnimation(FloatValueHolder(0F), 1F).setSpring(colorSpringForce) val radius = (background as GradientDrawable).cornerRadius val cornerRadiusDiff = ringerButtonUiModel.cornerRadius - (background as GradientDrawable).cornerRadius + + roundnessAnimation.minimumVisibleChange = BUTTON_MIN_VISIBLE_CHANGE + colorAnimation.minimumVisibleChange = BUTTON_MIN_VISIBLE_CHANGE coroutineScope { launch { colorAnimation.suspendAnimate { value -> diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt index 46d7d5f680ce..428dc6ecb5b6 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt @@ -80,7 +80,6 @@ constructor( MutableStateFlow(WindowInsets.Builder().build()) // Root view of the Volume Dialog. val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog_root) - root.alpha = 0f animateVisibility(root, dialog, viewModel.dialogVisibilityModel) diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java index 9d9fb9c23a73..6ad2128759a0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java @@ -30,9 +30,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.content.pm.ActivityInfo; @@ -51,11 +48,8 @@ import android.view.SurfaceControlViewHost; import android.view.View; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.Interpolator; import android.window.InputTransferToken; -import androidx.annotation.NonNull; import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; @@ -80,19 +74,17 @@ import java.util.function.Supplier; @RunWith(AndroidTestingRunner.class) @FlakyTest(bugId = 385115361) public class FullscreenMagnificationControllerTest extends SysuiTestCase { - private static final long ANIMATION_DURATION_MS = 100L; private static final long WAIT_TIMEOUT_S = 5L * HW_TIMEOUT_MULTIPLIER; - private static final long ANIMATION_TIMEOUT_MS = - 5L * ANIMATION_DURATION_MS * HW_TIMEOUT_MULTIPLIER; private static final String UNIQUE_DISPLAY_ID_PRIMARY = "000"; private static final String UNIQUE_DISPLAY_ID_SECONDARY = "111"; private static final int CORNER_RADIUS_PRIMARY = 10; private static final int CORNER_RADIUS_SECONDARY = 20; + private static final int DISABLED = 0; + private static final int ENABLED = 3; private FullscreenMagnificationController mFullscreenMagnificationController; private SurfaceControlViewHost mSurfaceControlViewHost; - private ValueAnimator mShowHideBorderAnimator; private SurfaceControl.Transaction mTransaction; private TestableWindowManager mWindowManager; @Mock @@ -136,7 +128,6 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); mTransaction = new SurfaceControl.Transaction(); - mShowHideBorderAnimator = spy(newNullTargetObjectAnimator()); mFullscreenMagnificationController = new FullscreenMagnificationController( mContext, mContext.getMainThreadHandler(), @@ -146,141 +137,68 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { mContext.getSystemService(WindowManager.class), mIWindowManager, scvhSupplier, - mTransaction, - mShowHideBorderAnimator); + mTransaction); } @After public void tearDown() { - getInstrumentation().runOnMainSync( - () -> mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(false)); + getInstrumentation().runOnMainSync(() -> + mFullscreenMagnificationController.cleanUpBorder()); + } + + @Test + public void createShowTargetAnimator_runAnimator_alphaIsEqualToOne() { + View view = new View(mContext); + view.setAlpha(0f); + ValueAnimator animator = mFullscreenMagnificationController.createShowTargetAnimator(view); + animator.end(); + assertThat(view.getAlpha()).isEqualTo(1f); + } + + @Test + public void createHideTargetAnimator_runAnimator_alphaIsEqualToZero() { + View view = new View(mContext); + view.setAlpha(1f); + ValueAnimator animator = mFullscreenMagnificationController.createHideTargetAnimator(view); + animator.end(); + assertThat(view.getAlpha()).isEqualTo(0f); } @Test - public void enableFullscreenMagnification_visibleBorder() + public void enableFullscreenMagnification_stateEnabled() throws InterruptedException, RemoteException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - verify(mShowHideBorderAnimator).start(); + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); + + assertThat(mFullscreenMagnificationController.getState()).isEqualTo(ENABLED); verify(mIWindowManager) .watchRotation(any(IRotationWatcher.class), eq(Display.DEFAULT_DISPLAY)); - assertThat(mSurfaceControlViewHost.getView().isVisibleToUser()).isTrue(); } @Test - public void disableFullscreenMagnification_reverseAnimationAndReleaseScvh() + public void disableFullscreenMagnification_stateDisabled() throws InterruptedException, RemoteException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch enableAnimationEndLatch = new CountDownLatch(1); - CountDownLatch disableAnimationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { - if (isReverse) { - disableAnimationEndLatch.countDown(); - } else { - enableAnimationEndLatch.countDown(); - } - } - }); - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for enabling animation to be finished") - .that(enableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - verify(mShowHideBorderAnimator).start(); + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); - getInstrumentation().runOnMainSync(() -> - // Disable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(false)); + getInstrumentation().runOnMainSync(() -> { + // Disable fullscreen magnification + mFullscreenMagnificationController + .onFullscreenMagnificationActivationChanged(false); + }); + waitForIdleSync(); + assertThat(mFullscreenMagnificationController.mShowHideBorderAnimator).isNotNull(); + mFullscreenMagnificationController.mShowHideBorderAnimator.end(); + waitForIdleSync(); - assertWithMessage("Failed to wait for disabling animation to be finished") - .that(disableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - verify(mShowHideBorderAnimator).reverse(); + assertThat(mFullscreenMagnificationController.getState()).isEqualTo(DISABLED); verify(mSurfaceControlViewHost).release(); verify(mIWindowManager).removeRotationWatcher(any(IRotationWatcher.class)); } @Test - public void onFullscreenMagnificationActivationChangeTrue_deactivating_reverseAnimator() - throws InterruptedException { - // Simulate the hiding border animation is running - when(mShowHideBorderAnimator.isRunning()).thenReturn(true); - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); - - getInstrumentation().runOnMainSync( - () -> mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - verify(mShowHideBorderAnimator).reverse(); - } - - @Test public void onScreenSizeChanged_activated_borderChangedToExpectedSize() throws InterruptedException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); + final Rect testWindowBounds = new Rect( mWindowManager.getCurrentWindowMetrics().getBounds()); testWindowBounds.set(testWindowBounds.left, testWindowBounds.top, @@ -304,29 +222,8 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { @Test public void enableFullscreenMagnification_applyPrimaryCornerRadius() throws InterruptedException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - - // Verify the initial corner radius is applied GradientDrawable backgroundDrawable = (GradientDrawable) mSurfaceControlViewHost.getView().getBackground(); assertThat(backgroundDrawable.getCornerRadius()).isEqualTo(CORNER_RADIUS_PRIMARY); @@ -334,28 +231,8 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { @EnableFlags(Flags.FLAG_UPDATE_CORNER_RADIUS_ON_DISPLAY_CHANGED) @Test - public void onDisplayChanged_updateCornerRadiusToSecondary() throws InterruptedException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); - - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); + public void onDisplayChanged_applyCornerRadiusToBorder() throws InterruptedException { + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); ArgumentCaptor<DisplayManager.DisplayListener> displayListenerCaptor = ArgumentCaptor.forClass(DisplayManager.DisplayListener.class); @@ -372,22 +249,34 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { .addOverride( com.android.internal.R.dimen.rounded_corner_radius, CORNER_RADIUS_SECONDARY); + getInstrumentation().runOnMainSync(() -> displayListenerCaptor.getValue().onDisplayChanged(Display.DEFAULT_DISPLAY)); waitForIdleSync(); + // Verify the corner radius is updated GradientDrawable backgroundDrawable2 = (GradientDrawable) mSurfaceControlViewHost.getView().getBackground(); assertThat(backgroundDrawable2.getCornerRadius()).isEqualTo(CORNER_RADIUS_SECONDARY); } + private void enableFullscreenMagnificationAndWaitForTransactionAndAnimation() + throws InterruptedException { + CountDownLatch transactionCommittedLatch = new CountDownLatch(1); + mTransaction.addTransactionCommittedListener( + Runnable::run, transactionCommittedLatch::countDown); + + getInstrumentation().runOnMainSync(() -> + //Enable fullscreen magnification + mFullscreenMagnificationController + .onFullscreenMagnificationActivationChanged(true)); - private ValueAnimator newNullTargetObjectAnimator() { - final ValueAnimator animator = - ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f); - Interpolator interpolator = new DecelerateInterpolator(2.5f); - animator.setInterpolator(interpolator); - animator.setDuration(ANIMATION_DURATION_MS); - return animator; + assertWithMessage("Failed to wait for transaction committed") + .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) + .isTrue(); + waitForIdleSync(); + assertThat(mFullscreenMagnificationController.mShowHideBorderAnimator).isNotNull(); + mFullscreenMagnificationController.mShowHideBorderAnimator.end(); + waitForIdleSync(); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java index 856c37934251..9f6ad56335d7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java @@ -82,7 +82,7 @@ public class MenuAnimationControllerTest extends SysuiTestCase { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); - final SecureSettings secureSettings = TestUtils.mockSecureSettings(); + final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, secureSettings, mHearingAidDeviceManager); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index 33cfb3890e71..1500340c9d89 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -144,7 +144,7 @@ public class MenuViewLayerTest extends SysuiTestCase { private HearingAidDeviceManager mHearingAidDeviceManager; @Mock private PackageManager mMockPackageManager; - private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(); + private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(mContext); private final NotificationManager mMockNotificationManager = mock(NotificationManager.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt index 37eb148a5ea7..fd751d9cc7c3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt @@ -466,6 +466,28 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { assertNull(runner.delegate) } + @Test + fun concurrentListenerModification_doesNotThrow() { + // Need a second listener to trigger the concurrent modification. + activityTransitionAnimator.addListener(object : ActivityTransitionAnimator.Listener {}) + `when`(listener.onTransitionAnimationStart()).thenAnswer { + activityTransitionAnimator.removeListener(listener) + listener + } + + val runner = activityTransitionAnimator.createEphemeralRunner(controller) + runner.onAnimationStart( + TRANSIT_NONE, + arrayOf(fakeWindow()), + emptyArray(), + emptyArray(), + iCallback, + ) + + waitForIdleSync() + verify(listener).onTransitionAnimationStart() + } + private fun controllerFactory( cookie: ActivityTransitionAnimator.TransitionCookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt index 8ae1332c387a..639bb691f455 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt @@ -18,13 +18,18 @@ package com.android.systemui.qs.panels.ui.viewmodel.toolbar import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.plugins.activityStarter import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel val Kosmos.editModeButtonViewModelFactory by Kosmos.Fixture { object : EditModeButtonViewModel.Factory { override fun create(): EditModeButtonViewModel { - return EditModeButtonViewModel(editModeViewModel, falsingInteractor) + return EditModeButtonViewModel( + editModeViewModel, + falsingInteractor, + activityStarter, + ) } } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt index ab193d294b8c..b3d89dbb834d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt @@ -203,6 +203,7 @@ class ShadeTestUtilSceneImpl( val isUserInputOngoing = MutableStateFlow(true) override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) { + shadeRepository.setLegacyIsQsExpanded(qsExpansion > 0f) if (shadeExpansion == 1f) { setIdleScene(Scenes.Shade) } else if (qsExpansion == 1f) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt index 4af5e7d9d725..6e44df833582 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt @@ -23,11 +23,21 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker import com.android.systemui.shade.ShadeWindowLayoutParams import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository +import java.util.Optional +import org.mockito.kotlin.any import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever val Kosmos.shadeLayoutParams by Kosmos.Fixture { ShadeWindowLayoutParams.create(mockedContext) } -val Kosmos.mockedWindowContext by Kosmos.Fixture { mock<WindowContext>() } +val Kosmos.mockedWindowContext by + Kosmos.Fixture { + mock<WindowContext>().apply { + whenever(reparentToDisplay(any())).thenAnswer { displayIdParam -> + whenever(displayId).thenReturn(displayIdParam.arguments[0] as Int) + } + } + } val Kosmos.mockedShadeDisplayChangeLatencyTracker by Kosmos.Fixture { mock<ShadeDisplayChangeLatencyTracker>() } val Kosmos.shadeDisplaysInteractor by @@ -38,5 +48,6 @@ val Kosmos.shadeDisplaysInteractor by testScope.backgroundScope, testScope.backgroundScope.coroutineContext, mockedShadeDisplayChangeLatencyTracker, + Optional.of(shadeExpandedStateInteractor), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt index af6d6249b4a8..1dc7229a6506 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.ShadeModule @@ -30,6 +31,7 @@ import com.android.systemui.statusbar.phone.dozeParameters import com.android.systemui.statusbar.policy.data.repository.userSetupRepository import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor import com.android.systemui.user.domain.interactor.userSwitcherInteractor +import org.mockito.kotlin.mock var Kosmos.baseShadeInteractor: BaseShadeInteractor by Kosmos.Fixture { @@ -71,3 +73,7 @@ val Kosmos.shadeInteractorImpl by shadeModeInteractor = shadeModeInteractor, ) } +var Kosmos.mockShadeInteractor: ShadeInteractor by Kosmos.Fixture { mock() } +val Kosmos.shadeExpandedStateInteractor by + Kosmos.Fixture { ShadeExpandedStateInteractorImpl(shadeInteractor, testScope.backgroundScope) } +val Kosmos.fakeShadeExpandedStateInteractor by Kosmos.Fixture { FakeShadeExpandedStateInteractor() } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index d9c105c512fa..0603c4506cd1 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -14600,7 +14600,7 @@ public class ActivityManagerService extends IActivityManager.Stub app.mProfile.addHostingComponentType(HOSTING_COMPONENT_TYPE_INSTRUMENTATION); } - app.setActiveInstrumentation(activeInstr); + mProcessStateController.setActiveInstrumentation(app, activeInstr); activeInstr.mFinished = false; activeInstr.mSourceUid = callingUid; activeInstr.mRunningProcesses.add(app); @@ -14746,7 +14746,7 @@ public class ActivityManagerService extends IActivityManager.Stub abiOverride, ZYGOTE_POLICY_FLAG_EMPTY); - app.setActiveInstrumentation(activeInstr); + mProcessStateController.setActiveInstrumentation(app, activeInstr); activeInstr.mFinished = false; activeInstr.mSourceUid = callingUid; activeInstr.mRunningProcesses.add(app); @@ -14883,7 +14883,7 @@ public class ActivityManagerService extends IActivityManager.Stub } instr.removeProcess(app); - app.setActiveInstrumentation(null); + mProcessStateController.setActiveInstrumentation(app, null); } app.mProfile.clearHostingComponentType(HOSTING_COMPONENT_TYPE_INSTRUMENTATION); diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java index 6b24df4a1fa8..225c7ca2ca9e 100644 --- a/services/core/java/com/android/server/am/AppProfiler.java +++ b/services/core/java/com/android/server/am/AppProfiler.java @@ -2477,13 +2477,15 @@ public class AppProfiler { // This is the wildcard mode, where every process brought up for // the target instrumentation should be included. if (aInstr.mTargetInfo.packageName.equals(app.info.packageName)) { - app.setActiveInstrumentation(aInstr); + mService.mProcessStateController.setActiveInstrumentation(app, + aInstr); aInstr.mRunningProcesses.add(app); } } else { for (String proc : aInstr.mTargetProcesses) { if (proc.equals(app.processName)) { - app.setActiveInstrumentation(aInstr); + mService.mProcessStateController.setActiveInstrumentation(app, + aInstr); aInstr.mRunningProcesses.add(app); break; } diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 9c569db99797..3abcd4e7a143 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -156,7 +156,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.os.Trace; -import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; @@ -3401,13 +3400,10 @@ public class OomAdjuster { } private static int getCpuCapability(ProcessRecord app, long nowUptime) { + // Note: persistent processes get all capabilities, including CPU_TIME. final UidRecord uidRec = app.getUidRecord(); if (uidRec != null && uidRec.isCurAllowListed()) { - // Process has user visible activities. - return PROCESS_CAPABILITY_CPU_TIME; - } - if (UserHandle.isCore(app.uid)) { - // Make sure all system components are not frozen. + // Process is in the power allowlist. return PROCESS_CAPABILITY_CPU_TIME; } if (app.mState.getCachedHasVisibleActivities()) { @@ -3418,6 +3414,12 @@ public class OomAdjuster { // It running a short fgs, just give it cpu time. return PROCESS_CAPABILITY_CPU_TIME; } + if (app.mReceivers.numberOfCurReceivers() > 0) { + return PROCESS_CAPABILITY_CPU_TIME; + } + if (app.hasActiveInstrumentation()) { + return PROCESS_CAPABILITY_CPU_TIME; + } // TODO(b/370817323): Populate this method with all of the reasons to keep a process // unfrozen. return 0; diff --git a/services/core/java/com/android/server/am/ProcessStateController.java b/services/core/java/com/android/server/am/ProcessStateController.java index 57899228e6ad..f44fb06727cf 100644 --- a/services/core/java/com/android/server/am/ProcessStateController.java +++ b/services/core/java/com/android/server/am/ProcessStateController.java @@ -246,12 +246,11 @@ public class ProcessStateController { } /** - * Set what sched group to grant a process due to running a broadcast. - * {@link ProcessList.SCHED_GROUP_UNDEFINED} means the process is not running a broadcast. + * Sets an active instrumentation running within the given process. */ - public void setBroadcastSchedGroup(@NonNull ProcessRecord proc, int schedGroup) { - // TODO(b/302575389): Migrate state pulled from BroadcastQueue to a pushed model - throw new UnsupportedOperationException("Not implemented yet"); + public void setActiveInstrumentation(@NonNull ProcessRecord proc, + ActiveInstrumentation activeInstrumentation) { + proc.setActiveInstrumentation(activeInstrumentation); } /********************* Process Visibility State Events *********************/ @@ -587,6 +586,34 @@ public class ProcessStateController { psr.updateHasTopStartedAlmostPerceptibleServices(); } + /************************ Broadcast Receiver State Events **************************/ + /** + * Set what sched group to grant a process due to running a broadcast. + * {@link ProcessList.SCHED_GROUP_UNDEFINED} means the process is not running a broadcast. + */ + public void setBroadcastSchedGroup(@NonNull ProcessRecord proc, int schedGroup) { + // TODO(b/302575389): Migrate state pulled from BroadcastQueue to a pushed model + throw new UnsupportedOperationException("Not implemented yet"); + } + + /** + * Note that the process has started processing a broadcast receiver. + */ + public boolean incrementCurReceivers(@NonNull ProcessRecord app) { + // TODO(b/302575389): Migrate state pulled from ATMS to a pushed model + // maybe used ActivityStateFlags instead. + throw new UnsupportedOperationException("Not implemented yet"); + } + + /** + * Note that the process has finished processing a broadcast receiver. + */ + public boolean decrementCurReceivers(@NonNull ProcessRecord app) { + // TODO(b/302575389): Migrate state pulled from ATMS to a pushed model + // maybe used ActivityStateFlags instead. + throw new UnsupportedOperationException("Not implemented yet"); + } + /** * Builder for ProcessStateController. */ diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 23383a9c55c0..f9e4022f04a0 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -1395,7 +1395,9 @@ public final class PowerManagerService extends SystemService DisplayGroupPowerChangeListener displayGroupPowerChangeListener = new DisplayGroupPowerChangeListener(); mDisplayManagerInternal.registerDisplayGroupListener(displayGroupPowerChangeListener); - mDisplayManager.registerDisplayListener(new DisplayListener(), mHandler); + if (mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) { + mDisplayManager.registerDisplayListener(new DisplayListener(), mHandler); + } if(mDreamManager != null){ // This DreamManager method does not acquire a lock, so it should be safe to call. @@ -3852,6 +3854,10 @@ public final class PowerManagerService extends SystemService @GuardedBy("mLock") private void notifyScreenTimeoutPolicyChangesLocked() { + if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) { + return; + } + for (int idx = 0; idx < mPowerGroups.size(); idx++) { final int powerGroupId = mPowerGroups.keyAt(idx); final PowerGroup powerGroup = mPowerGroups.valueAt(idx); @@ -6011,6 +6017,11 @@ public final class PowerManagerService extends SystemService @Override // Binder call public void addScreenTimeoutPolicyListener(int displayId, IScreenTimeoutPolicyListener listener) { + if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) { + throw new IllegalStateException("Screen timeout policy listener API flag " + + "is not enabled"); + } + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, null); @@ -6042,6 +6053,11 @@ public final class PowerManagerService extends SystemService @Override // Binder call public void removeScreenTimeoutPolicyListener(int displayId, IScreenTimeoutPolicyListener listener) { + if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) { + throw new IllegalStateException("Screen timeout policy listener API flag " + + "is not enabled"); + } + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, null); diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java index 42dbb7974fe2..f46fa446a0ba 100644 --- a/services/core/java/com/android/server/power/ThermalManagerService.java +++ b/services/core/java/com/android/server/power/ThermalManagerService.java @@ -155,6 +155,9 @@ public class ThermalManagerService extends SystemService { @VisibleForTesting final TemperatureWatcher mTemperatureWatcher; + @VisibleForTesting + final AtomicBoolean mIsHalSkinForecastSupported = new AtomicBoolean(false); + private final ThermalHalWrapper.WrapperThermalChangedCallback mWrapperCallback = new ThermalHalWrapper.WrapperThermalChangedCallback() { @Override @@ -254,6 +257,18 @@ public class ThermalManagerService extends SystemService { } onTemperatureMapChangedLocked(); mTemperatureWatcher.getAndUpdateThresholds(); + // we only check forecast if a single SKIN sensor threshold is reported + synchronized (mTemperatureWatcher.mSamples) { + if (mTemperatureWatcher.mSevereThresholds.size() == 1) { + try { + mIsHalSkinForecastSupported.set( + Flags.allowThermalHalSkinForecast() + && !Float.isNaN(mHalWrapper.forecastSkinTemperature(10))); + } catch (UnsupportedOperationException e) { + Slog.i(TAG, "Thermal HAL does not support forecastSkinTemperature"); + } + } + } mHalReady.set(true); } } @@ -1092,6 +1107,8 @@ public class ThermalManagerService extends SystemService { protected abstract List<TemperatureThreshold> getTemperatureThresholds(boolean shouldFilter, int type); + protected abstract float forecastSkinTemperature(int forecastSeconds); + protected abstract boolean connectToHal(); protected abstract void dump(PrintWriter pw, String prefix); @@ -1124,8 +1141,16 @@ public class ThermalManagerService extends SystemService { @VisibleForTesting static class ThermalHalAidlWrapper extends ThermalHalWrapper implements IBinder.DeathRecipient { /* Proxy object for the Thermal HAL AIDL service. */ + + @GuardedBy("mHalLock") private IThermal mInstance = null; + private IThermal getHalInstance() { + synchronized (mHalLock) { + return mInstance; + } + } + /** Callback for Thermal HAL AIDL. */ private final IThermalChangedCallback mThermalCallbackAidl = new IThermalChangedCallback.Stub() { @@ -1169,154 +1194,183 @@ public class ThermalManagerService extends SystemService { @Override protected List<Temperature> getCurrentTemperatures(boolean shouldFilter, int type) { - synchronized (mHalLock) { - final List<Temperature> ret = new ArrayList<>(); - if (mInstance == null) { + final IThermal instance = getHalInstance(); + final List<Temperature> ret = new ArrayList<>(); + if (instance == null) { + return ret; + } + try { + final android.hardware.thermal.Temperature[] halRet = + shouldFilter ? instance.getTemperaturesWithType(type) + : instance.getTemperatures(); + if (halRet == null) { return ret; } - try { - final android.hardware.thermal.Temperature[] halRet = - shouldFilter ? mInstance.getTemperaturesWithType(type) - : mInstance.getTemperatures(); - if (halRet == null) { - return ret; + for (android.hardware.thermal.Temperature t : halRet) { + if (!Temperature.isValidStatus(t.throttlingStatus)) { + Slog.e(TAG, "Invalid temperature status " + t.throttlingStatus + + " received from AIDL HAL"); + t.throttlingStatus = Temperature.THROTTLING_NONE; } - for (android.hardware.thermal.Temperature t : halRet) { - if (!Temperature.isValidStatus(t.throttlingStatus)) { - Slog.e(TAG, "Invalid temperature status " + t.throttlingStatus - + " received from AIDL HAL"); - t.throttlingStatus = Temperature.THROTTLING_NONE; - } - if (shouldFilter && t.type != type) { - continue; - } - ret.add(new Temperature(t.value, t.type, t.name, t.throttlingStatus)); + if (shouldFilter && t.type != type) { + continue; } - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); - } catch (RemoteException e) { - Slog.e(TAG, "Couldn't getCurrentTemperatures, reconnecting", e); - connectToHal(); + ret.add(new Temperature(t.value, t.type, t.name, t.throttlingStatus)); + } + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't getCurrentTemperatures, reconnecting", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); } - return ret; } + return ret; } @Override protected List<CoolingDevice> getCurrentCoolingDevices(boolean shouldFilter, int type) { - synchronized (mHalLock) { - final List<CoolingDevice> ret = new ArrayList<>(); - if (mInstance == null) { + final IThermal instance = getHalInstance(); + final List<CoolingDevice> ret = new ArrayList<>(); + if (instance == null) { + return ret; + } + try { + final android.hardware.thermal.CoolingDevice[] halRet = shouldFilter + ? instance.getCoolingDevicesWithType(type) + : instance.getCoolingDevices(); + if (halRet == null) { return ret; } - try { - final android.hardware.thermal.CoolingDevice[] halRet = shouldFilter - ? mInstance.getCoolingDevicesWithType(type) - : mInstance.getCoolingDevices(); - if (halRet == null) { - return ret; + for (android.hardware.thermal.CoolingDevice t : halRet) { + if (!CoolingDevice.isValidType(t.type)) { + Slog.e(TAG, "Invalid cooling device type " + t.type + " from AIDL HAL"); + continue; } - for (android.hardware.thermal.CoolingDevice t : halRet) { - if (!CoolingDevice.isValidType(t.type)) { - Slog.e(TAG, "Invalid cooling device type " + t.type + " from AIDL HAL"); - continue; - } - if (shouldFilter && t.type != type) { - continue; - } - ret.add(new CoolingDevice(t.value, t.type, t.name)); + if (shouldFilter && t.type != type) { + continue; } - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); - } catch (RemoteException e) { - Slog.e(TAG, "Couldn't getCurrentCoolingDevices, reconnecting", e); - connectToHal(); + ret.add(new CoolingDevice(t.value, t.type, t.name)); + } + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't getCurrentCoolingDevices, reconnecting", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); } - return ret; } + return ret; } @Override @NonNull protected List<TemperatureThreshold> getTemperatureThresholds( boolean shouldFilter, int type) { - synchronized (mHalLock) { - final List<TemperatureThreshold> ret = new ArrayList<>(); - if (mInstance == null) { + final IThermal instance = getHalInstance(); + final List<TemperatureThreshold> ret = new ArrayList<>(); + if (instance == null) { + return ret; + } + try { + final TemperatureThreshold[] halRet = + shouldFilter ? instance.getTemperatureThresholdsWithType(type) + : instance.getTemperatureThresholds(); + if (halRet == null) { return ret; } - try { - final TemperatureThreshold[] halRet = - shouldFilter ? mInstance.getTemperatureThresholdsWithType(type) - : mInstance.getTemperatureThresholds(); - if (halRet == null) { - return ret; - } - if (shouldFilter) { - return Arrays.stream(halRet).filter(t -> t.type == type).collect( - Collectors.toList()); - } - return Arrays.asList(halRet); - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't getTemperatureThresholds due to invalid status", e); - } catch (RemoteException e) { - Slog.e(TAG, "Couldn't getTemperatureThresholds, reconnecting...", e); - connectToHal(); + if (shouldFilter) { + return Arrays.stream(halRet).filter(t -> t.type == type).collect( + Collectors.toList()); + } + return Arrays.asList(halRet); + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't getTemperatureThresholds due to invalid status", e); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't getTemperatureThresholds, reconnecting...", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); } - return ret; } + return ret; + } + + @Override + protected float forecastSkinTemperature(int forecastSeconds) { + final IThermal instance = getHalInstance(); + if (instance == null) { + return Float.NaN; + } + try { + return instance.forecastSkinTemperature(forecastSeconds); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't forecastSkinTemperature, reconnecting...", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); + } + } + return Float.NaN; } @Override protected boolean connectToHal() { synchronized (mHalLock) { - IBinder binder = Binder.allowBlocking(ServiceManager.waitForDeclaredService( - IThermal.DESCRIPTOR + "/default")); - initProxyAndRegisterCallback(binder); + return connectToHalIfNeededLocked(mInstance); } + } + + @GuardedBy("mHalLock") + protected boolean connectToHalIfNeededLocked(IThermal instance) { + if (instance != mInstance) { + // instance has been updated since last used + return true; + } + IBinder binder = Binder.allowBlocking(ServiceManager.waitForDeclaredService( + IThermal.DESCRIPTOR + "/default")); + initProxyAndRegisterCallbackLocked(binder); return mInstance != null; } @VisibleForTesting void initProxyAndRegisterCallback(IBinder binder) { synchronized (mHalLock) { - if (binder != null) { - mInstance = IThermal.Stub.asInterface(binder); + initProxyAndRegisterCallbackLocked(binder); + } + } + + @GuardedBy("mHalLock") + protected void initProxyAndRegisterCallbackLocked(IBinder binder) { + if (binder != null) { + mInstance = IThermal.Stub.asInterface(binder); + try { + binder.linkToDeath(this, 0); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); + connectToHal(); + } + if (mInstance != null) { try { - binder.linkToDeath(this, 0); + Slog.i(TAG, "Thermal HAL AIDL service connected with version " + + mInstance.getInterfaceVersion()); } catch (RemoteException e) { - Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); + Slog.e(TAG, "Unable to read interface version from Thermal HAL", e); connectToHal(); + return; } - if (mInstance != null) { - try { - Slog.i(TAG, "Thermal HAL AIDL service connected with version " - + mInstance.getInterfaceVersion()); - } catch (RemoteException e) { - Slog.e(TAG, "Unable to read interface version from Thermal HAL", e); - connectToHal(); - return; - } - registerThermalChangedCallback(); + try { + mInstance.registerThermalChangedCallback(mThermalCallbackAidl); + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status", + e); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); + connectToHal(); } } } } - @VisibleForTesting - void registerThermalChangedCallback() { - try { - mInstance.registerThermalChangedCallback(mThermalCallbackAidl); - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status", - e); - } catch (RemoteException e) { - Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); - connectToHal(); - } - } - @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { @@ -1445,6 +1499,11 @@ public class ThermalManagerService extends SystemService { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + throw new UnsupportedOperationException("Not supported in Thermal HAL 1.0"); + } + + @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { pw.print(prefix); @@ -1583,6 +1642,11 @@ public class ThermalManagerService extends SystemService { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + throw new UnsupportedOperationException("Not supported in Thermal HAL 1.1"); + } + + @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { pw.print(prefix); @@ -1749,6 +1813,11 @@ public class ThermalManagerService extends SystemService { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + throw new UnsupportedOperationException("Not supported in Thermal HAL 2.0"); + } + + @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { pw.print(prefix); @@ -1977,6 +2046,39 @@ public class ThermalManagerService extends SystemService { float getForecast(int forecastSeconds) { synchronized (mSamples) { + // If we don't have any thresholds, we can't normalize the temperatures, + // so return early + if (mSevereThresholds.isEmpty()) { + Slog.e(TAG, "No temperature thresholds found"); + FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED, + Binder.getCallingUid(), + THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE_THRESHOLD, + Float.NaN, forecastSeconds); + return Float.NaN; + } + } + if (mIsHalSkinForecastSupported.get()) { + float threshold = -1f; + synchronized (mSamples) { + // we only do forecast if a single SKIN sensor threshold is reported + if (mSevereThresholds.size() == 1) { + threshold = mSevereThresholds.valueAt(0); + } + } + if (threshold > 0) { + try { + final float forecastTemperature = + mHalWrapper.forecastSkinTemperature(forecastSeconds); + return normalizeTemperature(forecastTemperature, threshold); + } catch (UnsupportedOperationException e) { + Slog.wtf(TAG, "forecastSkinTemperature returns unsupported"); + } catch (Exception e) { + Slog.e(TAG, "forecastSkinTemperature fails"); + } + return Float.NaN; + } + } + synchronized (mSamples) { mLastForecastCallTimeMillis = SystemClock.elapsedRealtime(); if (mSamples.isEmpty()) { getAndUpdateTemperatureSamples(); @@ -1993,17 +2095,6 @@ public class ThermalManagerService extends SystemService { return Float.NaN; } - // If we don't have any thresholds, we can't normalize the temperatures, - // so return early - if (mSevereThresholds.isEmpty()) { - Slog.e(TAG, "No temperature thresholds found"); - FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED, - Binder.getCallingUid(), - THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE_THRESHOLD, - Float.NaN, forecastSeconds); - return Float.NaN; - } - if (mCachedHeadrooms.contains(forecastSeconds)) { // TODO(b/360486877): replace with metrics Slog.d(TAG, diff --git a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java index 5cd7dee35e5f..42b44013bea2 100644 --- a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java +++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java @@ -37,6 +37,11 @@ public class PowerManagerFlags { Flags.FLAG_ENABLE_EARLY_SCREEN_TIMEOUT_DETECTOR, Flags::enableEarlyScreenTimeoutDetector); + private final FlagState mEnableScreenTimeoutPolicyListenerApi = new FlagState( + Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API, + Flags::enableScreenTimeoutPolicyListenerApi + ); + private final FlagState mImproveWakelockLatency = new FlagState( Flags.FLAG_IMPROVE_WAKELOCK_LATENCY, Flags::improveWakelockLatency @@ -63,6 +68,11 @@ public class PowerManagerFlags { return mEarlyScreenTimeoutDetectorFlagState.isEnabled(); } + /** Returns whether screen timeout policy listener APIs are enabled on not. */ + public boolean isScreenTimeoutPolicyListenerApiEnabled() { + return mEnableScreenTimeoutPolicyListenerApi.isEnabled(); + } + /** * @return Whether to improve the wakelock acquire/release latency or not */ diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig index a975da32f2fd..613daf820e34 100644 --- a/services/core/java/com/android/server/power/feature/power_flags.aconfig +++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig @@ -12,6 +12,17 @@ flag { } flag { + name: "enable_screen_timeout_policy_listener_api" + namespace: "power" + description: "Enables APIs that allow to listen to screen timeout policy changes" + bug: "363174979" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "improve_wakelock_latency" namespace: "power" description: "Feature flag for tracking the optimizations to improve the latency of acquiring and releasing a wakelock." diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java index d69150d88e4f..a1f72be7a039 100644 --- a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java @@ -15,7 +15,6 @@ */ package com.android.server.selinux; -import android.provider.DeviceConfig; import android.text.TextUtils; import android.util.Slog; @@ -34,10 +33,6 @@ class SelinuxAuditLogBuilder { private static final String TAG = "SelinuxAuditLogs"; - // This config indicates which Selinux logs for source domains to collect. The string will be - // inserted into a regex, so it must follow the regex syntax. For example, a valid value would - // be "system_server|untrusted_app". - @VisibleForTesting static final String CONFIG_SELINUX_AUDIT_DOMAIN = "selinux_audit_domain"; private static final Matcher NO_OP_MATCHER = Pattern.compile("no-op^").matcher(""); private static final String TCONTEXT_PATTERN = "u:object_r:(?<ttype>\\w+):s0(:c)?(?<tcategories>((,c)?\\d+)+)*"; @@ -50,7 +45,7 @@ class SelinuxAuditLogBuilder { private Iterator<String> mTokens; private final SelinuxAuditLog mAuditLog = new SelinuxAuditLog(); - SelinuxAuditLogBuilder() { + SelinuxAuditLogBuilder(String auditDomain) { Matcher scontextMatcher = NO_OP_MATCHER; Matcher tcontextMatcher = NO_OP_MATCHER; Matcher pathMatcher = NO_OP_MATCHER; @@ -59,10 +54,7 @@ class SelinuxAuditLogBuilder { Pattern.compile( TextUtils.formatSimple( "u:r:(?<stype>%s):s0(:c)?(?<scategories>((,c)?\\d+)+)*", - DeviceConfig.getString( - DeviceConfig.NAMESPACE_ADSERVICES, - CONFIG_SELINUX_AUDIT_DOMAIN, - "no_match^"))) + auditDomain)) .matcher(""); tcontextMatcher = Pattern.compile(TCONTEXT_PATTERN).matcher(""); pathMatcher = Pattern.compile(PATH_PATTERN).matcher(""); diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java index c655d46eb9f4..0aa705892376 100644 --- a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java @@ -15,6 +15,7 @@ */ package com.android.server.selinux; +import android.provider.DeviceConfig; import android.util.EventLog; import android.util.EventLog.Event; import android.util.Log; @@ -32,6 +33,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -43,9 +45,16 @@ class SelinuxAuditLogsCollector { private static final String SELINUX_PATTERN = "^.*\\bavc:\\s+(?<denial>.*)$"; + // This config indicates which Selinux logs for source domains to collect. The string will be + // inserted into a regex, so it must follow the regex syntax. For example, a valid value would + // be "system_server|untrusted_app". + @VisibleForTesting static final String CONFIG_SELINUX_AUDIT_DOMAIN = "selinux_audit_domain"; + @VisibleForTesting static final String DEFAULT_SELINUX_AUDIT_DOMAIN = "no_match^"; + @VisibleForTesting static final Matcher SELINUX_MATCHER = Pattern.compile(SELINUX_PATTERN).matcher(""); + private final Supplier<String> mAuditDomainSupplier; private final RateLimiter mRateLimiter; private final QuotaLimiter mQuotaLimiter; @@ -53,11 +62,26 @@ class SelinuxAuditLogsCollector { AtomicBoolean mStopRequested = new AtomicBoolean(false); - SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) { + SelinuxAuditLogsCollector( + Supplier<String> auditDomainSupplier, + RateLimiter rateLimiter, + QuotaLimiter quotaLimiter) { + mAuditDomainSupplier = auditDomainSupplier; mRateLimiter = rateLimiter; mQuotaLimiter = quotaLimiter; } + SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) { + this( + () -> + DeviceConfig.getString( + DeviceConfig.NAMESPACE_ADSERVICES, + CONFIG_SELINUX_AUDIT_DOMAIN, + DEFAULT_SELINUX_AUDIT_DOMAIN), + rateLimiter, + quotaLimiter); + } + public void setStopRequested(boolean stopRequested) { mStopRequested.set(stopRequested); } @@ -108,7 +132,8 @@ class SelinuxAuditLogsCollector { } private boolean writeAuditLogs(Queue<Event> logLines) { - final SelinuxAuditLogBuilder auditLogBuilder = new SelinuxAuditLogBuilder(); + final SelinuxAuditLogBuilder auditLogBuilder = + new SelinuxAuditLogBuilder(mAuditDomainSupplier.get()); int auditsWritten = 0; while (!mStopRequested.get() && !logLines.isEmpty()) { diff --git a/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java b/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java new file mode 100644 index 000000000000..2e73829ca143 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java @@ -0,0 +1,649 @@ +/* + * Copyright (C) 2024 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.timezonedetector; + +import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; +import static android.app.PendingIntent.FLAG_IMMUTABLE; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; +import static android.content.Context.RECEIVER_NOT_EXPORTED; +import static android.provider.Settings.ACTION_DATE_SETTINGS; + +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_UNKNOWN; + +import android.annotation.DurationMillisLong; +import android.annotation.IntDef; +import android.annotation.RequiresPermission; +import android.annotation.UserIdInt; +import android.app.ActivityManagerInternal; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.icu.text.DateFormat; +import android.icu.text.SimpleDateFormat; +import android.icu.util.TimeZone; +import android.os.Handler; +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.IndentingPrintWriter; +import android.util.Log; + +import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.notification.SystemNotificationChannels; +import com.android.server.LocalServices; +import com.android.server.flags.Flags; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * An implementation of {@link TimeZoneChangeListener} that fires notifications. + */ +public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { + @IntDef({STATUS_UNKNOWN, STATUS_UNTRACKED, STATUS_REJECTED, + STATUS_ACCEPTED, STATUS_SUPERSEDED}) + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) + @interface TimeZoneChangeStatus {} + + /** Used to indicate the status could not be inferred. */ + @TimeZoneChangeStatus + static final int STATUS_UNKNOWN = 0; + /** Used to indicate the change is not one that needs to be tracked. */ + @TimeZoneChangeStatus + static final int STATUS_UNTRACKED = 1; + @TimeZoneChangeStatus + static final int STATUS_REJECTED = 2; + @TimeZoneChangeStatus + static final int STATUS_ACCEPTED = 3; + /** Used to indicate a change was superseded before its status could be determined. */ + @TimeZoneChangeStatus + static final int STATUS_SUPERSEDED = 4; + + @IntDef({SIGNAL_TYPE_UNKNOWN, SIGNAL_TYPE_NONE, SIGNAL_TYPE_NOTIFICATION, + SIGNAL_TYPE_HEURISTIC}) + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) + @interface SignalType {} + + /** Used when the signal type cannot be inferred. */ + @SignalType + static final int SIGNAL_TYPE_UNKNOWN = 0; + /** Used when the status is not one that needs a signal type. */ + @SignalType + static final int SIGNAL_TYPE_NONE = 1; + @SignalType + static final int SIGNAL_TYPE_NOTIFICATION = 2; + @SignalType + static final int SIGNAL_TYPE_HEURISTIC = 3; + + private static final int MAX_EVENTS_TO_TRACK = 10; + + @VisibleForTesting + @DurationMillisLong + static final long AUTO_REVERT_THRESHOLD = Duration.ofMinutes(15).toMillis(); + + private static final String TAG = "TimeZoneChangeTracker"; + private static final String NOTIFICATION_TAG = "TimeZoneDetector"; + private static final int TZ_CHANGE_NOTIFICATION_ID = 1001; + + private static final String ACTION_NOTIFICATION_DELETED = + "com.android.server.timezonedetector.TimeZoneNotificationDeleted"; + + private static final String NOTIFICATION_INTENT_EXTRA_USER_ID = "user_id"; + private static final String NOTIFICATION_INTENT_EXTRA_CHANGE_ID = "change_id"; + + private final Context mContext; + private final NotificationManager mNotificationManager; + private final ActivityManagerInternal mActivityManagerInternal; + + // For scheduling callbacks + private final Handler mHandler; + private final ServiceConfigAccessor mServiceConfigAccessor; + private final AtomicInteger mNextChangeEventId = new AtomicInteger(1); + + private final Resources mRes = Resources.getSystem(); + + @GuardedBy("mTimeZoneChangeRecord") + private final ReferenceWithHistory<TimeZoneChangeRecord> mTimeZoneChangeRecord = + new ReferenceWithHistory<>(MAX_EVENTS_TO_TRACK); + + private final BroadcastReceiver mNotificationReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case ACTION_NOTIFICATION_DELETED: + int notifiedUserId = intent.getIntExtra( + NOTIFICATION_INTENT_EXTRA_USER_ID, UserHandle.USER_NULL); + int changeEventId = intent.getIntExtra( + NOTIFICATION_INTENT_EXTRA_CHANGE_ID, 0); + notificationSwipedAway(notifiedUserId, changeEventId); + break; + default: + Log.d(TAG, "Unknown intent action received: " + intent.getAction()); + } + } + }; + + private final Object mConfigurationLock = new Object(); + @GuardedBy("mConfigurationLock") + private ConfigurationInternal mConfigurationInternal; + @GuardedBy("mConfigurationLock") + private boolean mIsRegistered; + + private int mAcceptedManualChanges; + private int mAcceptedTelephonyChanges; + private int mAcceptedLocationChanges; + private int mAcceptedUnknownChanges; + private int mRejectedTelephonyChanges; + private int mRejectedLocationChanges; + private int mRejectedUnknownChanges; + + /** Create and initialise a new {@code TimeZoneChangeTrackerImpl} */ + @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL") + public static NotifyingTimeZoneChangeListener create(Handler handler, Context context, + ServiceConfigAccessor serviceConfigAccessor) { + NotifyingTimeZoneChangeListener changeTracker = + new NotifyingTimeZoneChangeListener(handler, + context, + serviceConfigAccessor, + context.getSystemService(NotificationManager.class)); + + // Pretend there was an update to initialize configuration. + changeTracker.handleConfigurationUpdate(); + + return changeTracker; + } + + @VisibleForTesting + NotifyingTimeZoneChangeListener( + Handler handler, Context context, ServiceConfigAccessor serviceConfigAccessor, + NotificationManager notificationManager) { + mHandler = Objects.requireNonNull(handler); + mContext = Objects.requireNonNull(context); + mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor); + mServiceConfigAccessor.addConfigurationInternalChangeListener( + this::handleConfigurationUpdate); + mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + mNotificationManager = notificationManager; + } + + @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL") + private void handleConfigurationUpdate() { + synchronized (mConfigurationLock) { + ConfigurationInternal oldConfigurationInternal = mConfigurationInternal; + mConfigurationInternal = mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + + if (areNotificationsEnabled() && isNotificationTrackingSupported()) { + if (!mIsRegistered) { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_NOTIFICATION_DELETED); + mContext.registerReceiverForAllUsers(mNotificationReceiver, intentFilter, + /* broadcastPermission= */ null, mHandler, RECEIVER_NOT_EXPORTED); + mIsRegistered = true; + } + } else if (mIsRegistered) { + mContext.unregisterReceiver(mNotificationReceiver); + mIsRegistered = false; + } + + if (oldConfigurationInternal != null) { + boolean userChanged = + oldConfigurationInternal.getUserId() != mConfigurationInternal.getUserId(); + + if (!areNotificationsEnabled() || userChanged) { + // Clear any notifications that are no longer needed. + clearNotificationForUser(oldConfigurationInternal.getUserId()); + } + } + } + } + + private void notificationSwipedAway(@UserIdInt int userId, int changeEventId) { + // User swiping away a notification is interpreted as "user accepted the change". + if (isNotificationTrackingSupported()) { + markChangeAsAccepted(changeEventId, userId, SIGNAL_TYPE_NOTIFICATION); + } + } + + private boolean areNotificationsEnabled() { + synchronized (mConfigurationLock) { + return mConfigurationInternal.getNotificationsEnabledBehavior(); + } + } + + private boolean isNotificationTrackingSupported() { + synchronized (mConfigurationLock) { + return mConfigurationInternal.isNotificationTrackingSupported(); + } + } + + private boolean isManualChangeTrackingSupported() { + synchronized (mConfigurationLock) { + return mConfigurationInternal.isManualChangeTrackingSupported(); + } + } + + /** + * Marks a change event as accepted by the user + * + * <p>A change event is said to be accepted when the client does not revert an automatic time + * zone change by manually changing the time zone within {@code AUTO_REVERT_THRESHOLD} of the + * notification being received. + */ + private void markChangeAsAccepted(int changeEventId, @UserIdInt int userId, + @SignalType int signalType) { + if (!isUserIdCurrentUser(userId)) { + return; + } + + synchronized (mTimeZoneChangeRecord) { + TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get(); + if (lastTimeZoneChangeRecord != null) { + if (lastTimeZoneChangeRecord.getId() != changeEventId) { + // To be accepted, the change being accepted has to still be the latest. + return; + } + if (lastTimeZoneChangeRecord.getStatus() != STATUS_UNKNOWN) { + // Change status has already been set. + return; + } + lastTimeZoneChangeRecord.setAccepted(signalType); + + switch (lastTimeZoneChangeRecord.getEvent().getOrigin()) { + case ORIGIN_MANUAL: + mAcceptedManualChanges += 1; + break; + case ORIGIN_TELEPHONY: + mAcceptedTelephonyChanges += 1; + break; + case ORIGIN_LOCATION: + mAcceptedLocationChanges += 1; + break; + default: + mAcceptedUnknownChanges += 1; + break; + } + } + } + } + + private boolean isUserIdCurrentUser(@UserIdInt int userId) { + synchronized (mConfigurationLock) { + return userId == mConfigurationInternal.getUserId(); + } + } + + /** + * Marks a change event as rejected by the user + * + * <p>A change event is said to be rejected when the client reverts an automatic time zone + * change by manually changing the time zone within {@code AUTO_REVERT_THRESHOLD} of the + * notification being received. + */ + @GuardedBy("mTimeZoneChangeRecord") + private void markChangeAsRejected(int changeEventId, @UserIdInt int userId, + @SignalType int signalType) { + if (!isUserIdCurrentUser(userId)) { + return; + } + + TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get(); + if (lastTimeZoneChangeRecord != null) { + if (lastTimeZoneChangeRecord.getId() != changeEventId) { + // To be accepted, the change being accepted has to still be the latest. + return; + } + if (lastTimeZoneChangeRecord.getStatus() != STATUS_UNKNOWN) { + // Change status has already been set. + return; + } + lastTimeZoneChangeRecord.setRejected(signalType); + + switch (lastTimeZoneChangeRecord.getEvent().getOrigin()) { + case ORIGIN_TELEPHONY: + mRejectedTelephonyChanges += 1; + break; + case ORIGIN_LOCATION: + mRejectedLocationChanges += 1; + break; + default: + mRejectedUnknownChanges += 1; + break; + } + } + } + + @Override + public void process(TimeZoneChangeEvent changeEvent) { + final TimeZoneChangeRecord trackedChangeEvent; + + synchronized (mTimeZoneChangeRecord) { + fixPotentialHistoryCorruption(changeEvent); + + TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get(); + int changeEventId = mNextChangeEventId.getAndIncrement(); + trackedChangeEvent = new TimeZoneChangeRecord(changeEventId, changeEvent); + + if (isManualChangeTrackingSupported()) { + // Time-based heuristic for "user is undoing a mistake made by the time zone + // detector". + if (lastTimeZoneChangeRecord != null + && lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) { + TimeZoneChangeEvent lastChangeEvent = lastTimeZoneChangeRecord.getEvent(); + + if (shouldRejectChangeEvent(changeEvent, lastChangeEvent)) { + markChangeAsRejected(lastTimeZoneChangeRecord.getId(), + changeEvent.getUserId(), SIGNAL_TYPE_HEURISTIC); + } + } + + // Schedule a callback for the new time zone so that we can implement "user accepted + // the change because they didn't revert it" + scheduleChangeAcceptedHeuristicCallback(trackedChangeEvent, AUTO_REVERT_THRESHOLD); + } + + if (lastTimeZoneChangeRecord != null + && lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) { + lastTimeZoneChangeRecord.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE); + } + + if (changeEvent.getOrigin() == ORIGIN_MANUAL) { + trackedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + } + + mTimeZoneChangeRecord.set(trackedChangeEvent); + } + + if (areNotificationsEnabled()) { + int currentUserId; + synchronized (mConfigurationLock) { + currentUserId = mConfigurationInternal.getUserId(); + } + + if (changeEvent.getOrigin() == ORIGIN_MANUAL) { + // Just clear any existing notification. + clearNotificationForUser(currentUserId); + } else { + notifyOfTimeZoneChange(currentUserId, trackedChangeEvent); + } + } + } + + /** + * Checks if the history of time zone change events is corrupted and fixes it, if needed + * + * <p>The history of changes is considered corrupted if a transition is missing. That is, if + * {@code events[i-1].newTimeZoneId != events[i].oldTimeZoneId}. In that case, a "synthetic" + * event is added to the history to bridge the gap between the last reported time zone ID and + * the time zone ID that the new event is replacing. + * + * <p>Note: we are not expecting this method to be required often (if ever) but in the + * eventuality that an event gets lost, we want to keep the history coherent. + */ + @GuardedBy("mTimeZoneChangeRecord") + private void fixPotentialHistoryCorruption(TimeZoneChangeEvent changeEvent) { + TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get(); + + if (lastTimeZoneChangeRecord != null) { + // The below block takes care of the case where we are missing record(s) of time + // zone changes + TimeZoneChangeEvent lastChangeEvent = lastTimeZoneChangeRecord.getEvent(); + if (!changeEvent.getOldZoneId().equals(lastChangeEvent.getNewZoneId())) { + int changeEventId = mNextChangeEventId.getAndIncrement(); + TimeZoneChangeEvent syntheticChangeEvent = new TimeZoneChangeEvent( + SystemClock.elapsedRealtime(), System.currentTimeMillis(), + ORIGIN_UNKNOWN, UserHandle.USER_NULL, lastChangeEvent.getNewZoneId(), + changeEvent.getOldZoneId(), 0, "Synthetic"); + TimeZoneChangeRecord syntheticTrackedChangeEvent = + new TimeZoneChangeRecord(changeEventId, syntheticChangeEvent); + syntheticTrackedChangeEvent.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE); + + mTimeZoneChangeRecord.set(syntheticTrackedChangeEvent); + + // Housekeeping for the last reported time zone change: try to ensure it has + // a status too. + if (lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) { + lastTimeZoneChangeRecord.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE); + } + } + } + } + + private static boolean shouldRejectChangeEvent(TimeZoneChangeEvent changeEvent, + TimeZoneChangeEvent lastChangeEvent) { + return changeEvent.getOrigin() == ORIGIN_MANUAL + && lastChangeEvent.getOrigin() != ORIGIN_MANUAL + && (changeEvent.getElapsedRealtimeMillis() + - lastChangeEvent.getElapsedRealtimeMillis() < AUTO_REVERT_THRESHOLD); + } + + private void scheduleChangeAcceptedHeuristicCallback( + TimeZoneChangeRecord trackedChangeEvent, + @DurationMillisLong long delayMillis) { + mHandler.postDelayed( + () -> changeAcceptedTimeHeuristicCallback(trackedChangeEvent.getId()), delayMillis); + } + + private void changeAcceptedTimeHeuristicCallback(int changeEventId) { + if (isManualChangeTrackingSupported()) { + int currentUserId = mActivityManagerInternal.getCurrentUserId(); + markChangeAsAccepted(changeEventId, currentUserId, SIGNAL_TYPE_HEURISTIC); + } + } + + private void clearNotificationForUser(@UserIdInt int userId) { + mNotificationManager.cancelAsUser(NOTIFICATION_TAG, TZ_CHANGE_NOTIFICATION_ID, + UserHandle.of(userId)); + } + + private void notifyOfTimeZoneChange(@UserIdInt int userId, + TimeZoneChangeRecord trackedChangeEvent) { + TimeZoneChangeEvent changeEvent = trackedChangeEvent.getEvent(); + + if (!Flags.datetimeNotifications() || !areNotificationsEnabled()) { + return; + } + + TimeZone oldTimeZone = TimeZone.getTimeZone(changeEvent.getOldZoneId()); + TimeZone newTimeZone = TimeZone.getTimeZone(changeEvent.getNewZoneId()); + long unixEpochTimeMillis = changeEvent.getUnixEpochTimeMillis(); + boolean hasOffsetChanged = newTimeZone.getOffset(unixEpochTimeMillis) + == oldTimeZone.getOffset(unixEpochTimeMillis); + + if (hasOffsetChanged) { + // If the time zone ID changes but not the offset, we do not send a notification to + // the user. This is to prevent spamming users and reduce the number of notification + // we send overall. + Log.d(TAG, "The time zone ID has changed but the offset remains the same."); + return; + } + + final CharSequence title = mRes.getString(R.string.time_zone_change_notification_title); + final CharSequence body = getNotificationBody(newTimeZone, unixEpochTimeMillis); + + final Intent clickNotificationIntent = new Intent(ACTION_DATE_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + final Intent clearNotificationIntent = new Intent(ACTION_NOTIFICATION_DELETED) + .putExtra(NOTIFICATION_INTENT_EXTRA_USER_ID, userId) + .putExtra(NOTIFICATION_INTENT_EXTRA_CHANGE_ID, trackedChangeEvent.getId()); + + Notification notification = new Notification.Builder(mContext, + SystemNotificationChannels.TIME) + .setSmallIcon(R.drawable.btn_clock_material) + .setStyle(new Notification.BigTextStyle().bigText(body)) + .setOnlyAlertOnce(true) + .setColor(mContext.getColor(R.color.system_notification_accent_color)) + .setTicker(title) + .setContentTitle(title) + .setContentText(body) + .setContentIntent(PendingIntent.getActivityAsUser( + mContext, + /* requestCode= */ 0, + clickNotificationIntent, + /* flags= */ FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, + /* options= */ null, + UserHandle.of(userId))) + .setDeleteIntent(PendingIntent.getBroadcast( + mContext, + /* requestCode= */ 0, + clearNotificationIntent, + /* flags= */ FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)) + .setAutoCancel(true) // auto-clear notification on selection + .build(); + + mNotificationManager.notifyAsUser(NOTIFICATION_TAG, + TZ_CHANGE_NOTIFICATION_ID, notification, UserHandle.of(userId)); + } + + private CharSequence getNotificationBody(TimeZone newTimeZone, long unixEpochTimeMillis) { + DateFormat timeFormat = SimpleDateFormat.getInstanceForSkeleton("zzzz"); + DateFormat offsetFormat = SimpleDateFormat.getInstanceForSkeleton("ZZZZ"); + + String newTime = formatInZone(timeFormat, newTimeZone, unixEpochTimeMillis); + String newOffset = formatInZone(offsetFormat, newTimeZone, unixEpochTimeMillis); + + return mRes.getString(R.string.time_zone_change_notification_body, newTime, newOffset); + } + + private static String formatInZone(DateFormat timeFormat, TimeZone timeZone, + long unixEpochTimeMillis) { + timeFormat.setTimeZone(timeZone); + return timeFormat.format(unixEpochTimeMillis); + } + + @Override + public void dump(IndentingPrintWriter pw) { + synchronized (mConfigurationLock) { + pw.println("currentUserId=" + mConfigurationInternal.getUserId()); + pw.println("notificationsEnabledBehavior=" + + mConfigurationInternal.getNotificationsEnabledBehavior()); + pw.println("notificationTrackingSupported=" + + mConfigurationInternal.isNotificationTrackingSupported()); + pw.println("manualChangeTrackingSupported=" + + mConfigurationInternal.isManualChangeTrackingSupported()); + } + + pw.println("mAcceptedLocationChanges=" + mAcceptedLocationChanges); + pw.println("mAcceptedManualChanges=" + mAcceptedManualChanges); + pw.println("mAcceptedTelephonyChanges=" + mAcceptedTelephonyChanges); + pw.println("mAcceptedUnknownChanges=" + mAcceptedUnknownChanges); + pw.println("mRejectedLocationChanges=" + mRejectedLocationChanges); + pw.println("mRejectedTelephonyChanges=" + mRejectedTelephonyChanges); + pw.println("mRejectedUnknownChanges=" + mRejectedUnknownChanges); + pw.println("mNextChangeEventId=" + mNextChangeEventId); + + pw.println("mTimeZoneChangeRecord:"); + pw.increaseIndent(); + synchronized (mTimeZoneChangeRecord) { + mTimeZoneChangeRecord.dump(pw); + } + pw.decreaseIndent(); + } + + @VisibleForTesting + static class TimeZoneChangeRecord { + + private final int mId; + private final TimeZoneChangeEvent mEvent; + private @TimeZoneChangeStatus int mStatus = STATUS_UNKNOWN; + private @SignalType int mSignalType = SIGNAL_TYPE_UNKNOWN; + + TimeZoneChangeRecord(int id, TimeZoneChangeEvent event) { + mId = id; + mEvent = Objects.requireNonNull(event); + } + + public int getId() { + return mId; + } + + public @TimeZoneChangeStatus int getStatus() { + return mStatus; + } + + public void setAccepted(int signalType) { + setStatus(STATUS_ACCEPTED, signalType); + } + + public void setRejected(int signalType) { + setStatus(STATUS_REJECTED, signalType); + } + + public void setStatus(@TimeZoneChangeStatus int status, @SignalType int signalType) { + mStatus = status; + mSignalType = signalType; + } + + public TimeZoneChangeEvent getEvent() { + return mEvent; + } + + @Override + public String toString() { + return "TrackedTimeZoneChangeEvent{" + + "mId=" + mId + + ", mEvent=" + mEvent + + ", mStatus=" + mStatus + + ", mSignalType=" + mSignalType + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof TimeZoneChangeRecord that) { + return mId == that.mId + && mEvent.equals(that.mEvent) + && mStatus == that.mStatus + && mSignalType == that.mSignalType; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(mId, mEvent, mStatus, mSignalType); + } + } + + @VisibleForTesting + TimeZoneChangeRecord getLastTimeZoneChangeRecord() { + synchronized (mTimeZoneChangeRecord) { + return mTimeZoneChangeRecord.get(); + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java index e14326cc2d53..d340ed470591 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java @@ -102,5 +102,29 @@ public interface TimeZoneChangeListener { + ", mCause='" + mCause + '\'' + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof TimeZoneChangeEvent that) { + return mElapsedRealtimeMillis == that.mElapsedRealtimeMillis + && mUnixEpochTimeMillis == that.mUnixEpochTimeMillis + && mOrigin == that.mOrigin + && mUserId == that.mUserId + && Objects.equals(mOldZoneId, that.mOldZoneId) + && Objects.equals(mNewZoneId, that.mNewZoneId) + && mNewConfidence == that.mNewConfidence + && Objects.equals(mCause, that.mCause); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(mElapsedRealtimeMillis, mUnixEpochTimeMillis, mOrigin, mUserId, + mOldZoneId, mNewZoneId, mNewConfidence, mCause); + } } } diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java index 19a28ddcdaeb..b2b06b0af5fa 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java @@ -42,6 +42,7 @@ import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; import android.content.Context; import android.os.Handler; +import android.os.SystemClock; import android.os.TimestampedValue; import android.os.UserHandle; import android.util.IndentingPrintWriter; @@ -50,6 +51,7 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.SystemTimeZone.TimeZoneConfidence; +import com.android.server.flags.Flags; import com.android.server.timezonedetector.ConfigurationInternal.DetectionMode; import java.io.PrintWriter; @@ -215,6 +217,13 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat private final List<StateChangeListener> mStateChangeListeners = new ArrayList<>(); /** + * A component adjunct to the detection behavior that tracks time zone changes and implements + * behavior associated with time zone changes. + */ + @NonNull + private final TimeZoneChangeListener mChangeTracker; + + /** * A snapshot of the current detector status. A local copy is cached because it is relatively * heavyweight to obtain and is used more often than it is expected to change. */ @@ -256,15 +265,20 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat @NonNull ServiceConfigAccessor serviceConfigAccessor) { Environment environment = new EnvironmentImpl(handler); - return new TimeZoneDetectorStrategyImpl(serviceConfigAccessor, environment); + TimeZoneChangeListener changeEventTracker = + NotifyingTimeZoneChangeListener.create(handler, context, serviceConfigAccessor); + return new TimeZoneDetectorStrategyImpl( + serviceConfigAccessor, environment, changeEventTracker); } @VisibleForTesting public TimeZoneDetectorStrategyImpl( @NonNull ServiceConfigAccessor serviceConfigAccessor, - @NonNull Environment environment) { + @NonNull Environment environment, + @NonNull TimeZoneChangeListener changeEventTracker) { mEnvironment = Objects.requireNonNull(environment); mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor); + mChangeTracker = Objects.requireNonNull(changeEventTracker); // Start with telephony fallback enabled. mTelephonyTimeZoneFallbackEnabled = @@ -833,6 +847,17 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat Slog.d(LOG_TAG, logInfo); } mEnvironment.setDeviceTimeZoneAndConfidence(newZoneId, newConfidence, logInfo); + + if (Flags.datetimeNotifications()) { + // Record the fact that the time zone was changed so that it can be tracked, i.e. + // whether the device / user sticks with it. + TimeZoneChangeListener.TimeZoneChangeEvent changeEvent = + new TimeZoneChangeListener.TimeZoneChangeEvent( + SystemClock.elapsedRealtime(), System.currentTimeMillis(), origin, + userId, + currentZoneId, newZoneId, newConfidence, cause); + mChangeTracker.process(changeEvent); + } } @GuardedBy("this") @@ -947,6 +972,14 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat ipw.increaseIndent(); // level 2 mTelephonySuggestionsBySlotIndex.dump(ipw); ipw.decreaseIndent(); // level 2 + + if (Flags.datetimeNotifications()) { + ipw.println("Time zone change tracker:"); + ipw.increaseIndent(); // level 2 + mChangeTracker.dump(ipw); + ipw.decreaseIndent(); // level 2 + } + ipw.decreaseIndent(); // level 1 } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 1fe61590a531..d31aed2aee37 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -8995,13 +8995,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A || orientationRespectedWithInsets)) { return; } - final AppCompatDisplayInsets mAppCompatDisplayInsets = getAppCompatDisplayInsets(); + final AppCompatDisplayInsets appCompatDisplayInsets = getAppCompatDisplayInsets(); final AppCompatSizeCompatModePolicy scmPolicy = mAppCompatController.getAppCompatSizeCompatModePolicy(); - if (scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance() - && mAppCompatDisplayInsets != null - && !mAppCompatDisplayInsets.mIsInFixedOrientationOrAspectRatioLetterbox) { + if (appCompatDisplayInsets != null + && !appCompatDisplayInsets.mIsInFixedOrientationOrAspectRatioLetterbox) { // App prefers to keep its original size. // If the size compat is from previous fixed orientation letterboxing, we may want to // have fixed orientation letterbox again, otherwise it will show the size compat @@ -9053,8 +9052,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A .applyDesiredAspectRatio(newParentConfig, parentBounds, resolvedBounds, containingBoundsWithInsets, containingBounds); - if (scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()) { - mAppCompatDisplayInsets.getBoundsByRotation(mTmpBounds, + if (appCompatDisplayInsets != null) { + appCompatDisplayInsets.getBoundsByRotation(mTmpBounds, newParentConfig.windowConfiguration.getRotation()); if (resolvedBounds.width() != mTmpBounds.width() || resolvedBounds.height() != mTmpBounds.height()) { @@ -9077,7 +9076,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Calculate app bounds using fixed orientation bounds because they will be needed later // for comparison with size compat app bounds in {@link resolveSizeCompatModeConfiguration}. - mResolveConfigHint.mTmpCompatInsets = mAppCompatDisplayInsets; + mResolveConfigHint.mTmpCompatInsets = appCompatDisplayInsets; computeConfigByResolveHint(getResolvedOverrideConfiguration(), newParentConfig); mAppCompatController.getAppCompatAspectRatioPolicy() .setLetterboxBoundsForFixedOrientationAndAspectRatio(new Rect(resolvedBounds)); diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index ef6f92317b2c..12c8f9ccac7c 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2538,7 +2538,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { void wakeUp(int displayId, String reason) { mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_APPLICATION, - "android.server.am:TURN_ON:" + reason, displayId); + "android.server.wm:TURN_ON:" + reason, displayId); } /** Starts a batch of visibility updates. */ diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java index 3c60d8296577..db058cafe5fe 100644 --- a/services/core/java/com/android/server/wm/DragDropController.java +++ b/services/core/java/com/android/server/wm/DragDropController.java @@ -215,7 +215,8 @@ class DragDropController { mDragState.mOriginalAlpha = alpha; mDragState.mAnimatedScale = callingWin.mGlobalScale; mDragState.mToken = dragToken; - mDragState.mDisplayContent = displayContent; + mDragState.mStartDragDisplayContent = displayContent; + mDragState.mCurrentDisplayContent = displayContent; mDragState.mData = data; mDragState.mCallingTaskIdToHide = shouldMoveCallingTaskToBack(callingWin, flags); @@ -273,7 +274,7 @@ class DragDropController { InputManagerGlobal.getInstance().setPointerIcon( PointerIcon.getSystemIcon( mService.mContext, PointerIcon.TYPE_GRABBING), - mDragState.mDisplayContent.getDisplayId(), touchDeviceId, + mDragState.mCurrentDisplayContent.getDisplayId(), touchDeviceId, touchPointerId, mDragState.getInputToken()); } // remember the thumb offsets for later diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java index b3e9244d108d..d48b9b4a5d10 100644 --- a/services/core/java/com/android/server/wm/DragState.java +++ b/services/core/java/com/android/server/wm/DragState.java @@ -129,10 +129,17 @@ class DragState { */ volatile boolean mAnimationCompleted = false; /** + * The display on which the drag originally started. Note that it's possible for either/both + * mStartDragDisplayContent and mCurrentDisplayContent to be invalid if DisplayTopology was + * changed or removed in the middle of the drag. In this case, drag will also be cancelled as + * soon as listener is notified. + */ + DisplayContent mStartDragDisplayContent; + /** * The display on which the drag is happening. If it goes into a different display this will * be updated. */ - DisplayContent mDisplayContent; + DisplayContent mCurrentDisplayContent; @Nullable private ValueAnimator mAnimator; private final Interpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f); @@ -181,7 +188,7 @@ class DragState { .setContainerLayer() .setName("Drag and Drop Input Consumer") .setCallsite("DragState.showInputSurface") - .setParent(mDisplayContent.getOverlayLayer()) + .setParent(mCurrentDisplayContent.getOverlayLayer()) .build(); } final InputWindowHandle h = getInputWindowHandle(); @@ -246,7 +253,8 @@ class DragState { } } DragEvent event = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED, inWindowX, - inWindowY, mThumbOffsetX, mThumbOffsetY, mFlags, null, null, null, + inWindowY, mThumbOffsetX, mThumbOffsetY, + mCurrentDisplayContent.getDisplayId(), mFlags, null, null, null, dragSurface, null, mDragResult); try { if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DRAG_ENDED to " + ws); @@ -549,7 +557,7 @@ class DragState { PointF relativeToWindowCoords = new PointF(newWin.translateToWindowX(touchX), newWin.translateToWindowY(touchY)); if (Flags.enableConnectedDisplaysDnd() - && mDisplayContent.getDisplayId() != newWin.getDisplayId()) { + && mCurrentDisplayContent.getDisplayId() != newWin.getDisplayId()) { // Currently DRAG_STARTED coords are sent relative to the window target in **px** // coordinates. However, this cannot be extended to connected displays scenario, // as there's only global **dp** coordinates and no global **px** coordinates. @@ -720,6 +728,20 @@ class DragState { mCurrentDisplayX = displayX; mCurrentDisplayY = displayY; + final DisplayContent lastSetDisplayContent = mCurrentDisplayContent; + boolean cursorMovedToDifferentDisplay = false; + // Keep latest display up-to-date even when drag has stopped. + if (Flags.enableConnectedDisplaysDnd() && mCurrentDisplayContent.mDisplayId != displayId) { + final DisplayContent newDisplay = mService.mRoot.getDisplayContent(displayId); + if (newDisplay == null) { + Slog.e(TAG_WM, "Target displayId=" + displayId + " was not found, ending drag."); + endDragLocked(false /* dropConsumed */, + false /* relinquishDragSurfaceToDropTarget */); + return; + } + cursorMovedToDifferentDisplay = true; + mCurrentDisplayContent = newDisplay; + } if (!keepHandling) { return; } @@ -728,6 +750,24 @@ class DragState { if (SHOW_LIGHT_TRANSACTIONS) { Slog.i(TAG_WM, ">>> OPEN TRANSACTION notifyMoveLocked"); } + if (cursorMovedToDifferentDisplay) { + mAnimatedScale = mAnimatedScale * mCurrentDisplayContent.mBaseDisplayDensity + / lastSetDisplayContent.mBaseDisplayDensity; + mThumbOffsetX = mThumbOffsetX * mCurrentDisplayContent.mBaseDisplayDensity + / lastSetDisplayContent.mBaseDisplayDensity; + mThumbOffsetY = mThumbOffsetY * mCurrentDisplayContent.mBaseDisplayDensity + / lastSetDisplayContent.mBaseDisplayDensity; + mTransaction.reparent(mSurfaceControl, mCurrentDisplayContent.getSurfaceControl()); + mTransaction.setScale(mSurfaceControl, mAnimatedScale, mAnimatedScale); + + final InputWindowHandle inputWindowHandle = getInputWindowHandle(); + if (inputWindowHandle == null) { + Slog.w(TAG_WM, "Drag is in progress but there is no drag window handle."); + return; + } + inputWindowHandle.displayId = displayId; + mTransaction.setInputWindowInfo(mInputSurface, inputWindowHandle); + } mTransaction.setPosition(mSurfaceControl, displayX - mThumbOffsetX, displayY - mThumbOffsetY).apply(); ProtoLog.i(WM_SHOW_TRANSACTIONS, "DRAG %s: displayId=%d, pos=(%d,%d)", mSurfaceControl, @@ -752,10 +792,10 @@ class DragState { ClipData data, boolean includeDragSurface, boolean includeDragFlags, IDragAndDropPermissions dragAndDropPermissions) { return DragEvent.obtain(action, x, y, mThumbOffsetX, mThumbOffsetY, - includeDragFlags ? mFlags : 0, + mCurrentDisplayContent.getDisplayId(), includeDragFlags ? mFlags : 0, null /* localState */, description, data, - includeDragSurface ? mSurfaceControl : null, - dragAndDropPermissions, false /* result */); + includeDragSurface ? mSurfaceControl : null, dragAndDropPermissions, + false /* result */); } private ValueAnimator createReturnAnimationLocked() { diff --git a/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java b/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java index 8c50913dd563..29922f0f85c5 100644 --- a/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java +++ b/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java @@ -57,9 +57,11 @@ class PageSizeMismatchDialog extends AppWarnings.BaseDialog { final AlertDialog.Builder builder = new AlertDialog.Builder(context) - .setPositiveButton( - R.string.ok, - (dialog, which) -> {/* Do nothing */}) + .setPositiveButton(R.string.ok, (dialog, which) -> + manager.setPackageFlag( + mUserId, mPackageName, + AppWarnings.FLAG_HIDE_PAGE_SIZE_MISMATCH, + true)) .setMessage(Html.fromHtml(warning, FROM_HTML_MODE_COMPACT)) .setTitle(label); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 9c1cf6e6bf62..fe478c60bc32 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5230,9 +5230,15 @@ class Task extends TaskFragment { // to ensure any necessary pause logic occurs. In the case where the Activity will be // shown regardless of the lock screen, the call to // {@link ActivityTaskSupervisor#checkReadyForSleepLocked} is skipped. - final ActivityRecord next = topRunningActivity(true /* focusableOnly */); - if (next == null || !next.canTurnScreenOn()) { - checkReadyForSleep(); + if (shouldSleepActivities()) { + final ActivityRecord next = topRunningActivity(true /* focusableOnly */); + if (next != null && next.canTurnScreenOn() + && !mWmService.mPowerManager.isInteractive()) { + mTaskSupervisor.wakeUp(getDisplayId(), "resumeTop-turnScreenOnFlag"); + next.setCurrentLaunchCanTurnScreenOn(false); + } else { + checkReadyForSleep(); + } } } finally { mInResumeTopActivity = false; diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 54a3d4179e3d..e761e024b3ca 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -117,6 +117,7 @@ import com.android.server.wm.SurfaceAnimator.Animatable; import com.android.server.wm.SurfaceAnimator.AnimationType; import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; import com.android.server.wm.utils.AlwaysTruePredicate; +import com.android.window.flags.Flags; import java.io.PrintWriter; import java.lang.ref.WeakReference; @@ -2736,6 +2737,13 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< if (!mTransitionController.canAssignLayers(this)) return; final boolean changed = layer != mLastLayer || mLastRelativeToLayer != null; if (mSurfaceControl != null && changed) { + if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) { + // When this container needs to be synced, assign layer with its own sync + // transaction to avoid out of ordering when merge. + // Still use the passed-in transaction for non-sync case, such as building finish + // transaction. + t = getSyncTransaction(); + } setLayer(t, layer); mLastLayer = layer; mLastRelativeToLayer = null; @@ -2746,6 +2754,13 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< boolean forceUpdate) { final boolean changed = layer != mLastLayer || mLastRelativeToLayer != relativeTo; if (mSurfaceControl != null && (changed || forceUpdate)) { + if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) { + // When this container needs to be synced, assign layer with its own sync + // transaction to avoid out of ordering when merge. + // Still use the passed-in transaction for non-sync case, such as building finish + // transaction. + t = getSyncTransaction(); + } setRelativeLayer(t, relativeTo, layer); mLastLayer = layer; mLastRelativeToLayer = relativeTo; diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index d69b06ad71ea..85e3d89730a3 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5562,6 +5562,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP @Override void assignLayer(Transaction t, int layer) { if (mStartingData != null) { + if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) { + // When this container needs to be synced, assign layer with its own sync + // transaction to avoid out of ordering when merge. + // Still use the passed-in transaction for non-sync case, such as building finish + // transaction. + t = getSyncTransaction(); + } // The starting window should cover the task. t.setLayer(mSurfaceControl, Integer.MAX_VALUE); return; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index d2d388401e23..0ce25db6ea55 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -10470,6 +10470,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // Reset some of the user-specific policies. final DevicePolicyData policy = getUserData(userId); policy.mPermissionPolicy = DevicePolicyManager.PERMISSION_POLICY_PROMPT; + mPolicyCache.setPermissionPolicy(userId, policy.mPermissionPolicy); // Clear delegations. policy.mDelegationMap.clear(); policy.mStatusBarDisabled = false; diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java index 6defadf44d05..35ab2d233563 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -1454,6 +1454,18 @@ public class ActivityManagerServiceTest { assertThat(tokenForFullIntent.getKeyFields()).isEqualTo(tokenForCloneIntent.getKeyFields()); } + @Test + public void testCanLaunchClipDataIntent() { + ClipData clipData = ClipData.newIntent("test", new Intent("test")); + clipData.prepareToLeaveProcess(true); + // skip mimicking sending clipData to another app because it will just be parceled and + // un-parceled. + Intent intent = clipData.getItemAt(0).getIntent(); + // default intent redirect protection won't block an intent nested in a top level ClipData. + assertThat(intent.getExtendedFlags() + & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isEqualTo(0); + } + private void verifyWaitingForNetworkStateUpdate(long curProcStateSeq, long lastNetworkUpdatedProcStateSeq, final long procStateSeqToWait, boolean expectWait) throws Exception { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 4a09802fc822..fe7cc923d3d1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -743,6 +743,43 @@ public class MockingOomAdjusterTests { @SuppressWarnings("GuardedBy") @Test + @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + public void testUpdateOomAdjFreezeState_receivers() { + final ProcessRecord app = makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, + MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true); + + updateOomAdj(app); + assertNoCpuTime(app); + + app.mReceivers.incrementCurReceivers(); + updateOomAdj(app); + assertCpuTime(app); + + app.mReceivers.decrementCurReceivers(); + updateOomAdj(app); + assertNoCpuTime(app); + } + + @SuppressWarnings("GuardedBy") + @Test + @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + public void testUpdateOomAdjFreezeState_activeInstrumentation() { + ProcessRecord app = makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME, + MOCKAPP_PACKAGENAME, true); + updateOomAdj(app); + assertNoCpuTime(app); + + mProcessStateController.setActiveInstrumentation(app, mock(ActiveInstrumentation.class)); + updateOomAdj(app); + assertCpuTime(app); + + mProcessStateController.setActiveInstrumentation(app, null); + updateOomAdj(app); + assertNoCpuTime(app); + } + + @SuppressWarnings("GuardedBy") + @Test public void testUpdateOomAdj_DoOne_OverlayUi() { ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java index f1072da4161f..6d91bee6d3f6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java @@ -573,4 +573,14 @@ public class ThermalManagerServiceMockingTest { assertNotNull(ret); assertEquals(0, ret.size()); } + + @Test + public void forecastSkinTemperature() throws RemoteException { + Mockito.when(mAidlHalMock.forecastSkinTemperature(Mockito.anyInt())).thenReturn( + 0.55f + ); + float forecast = mAidlWrapper.forecastSkinTemperature(10); + Mockito.verify(mAidlHalMock, Mockito.times(1)).forecastSkinTemperature(10); + assertEquals(0.55f, forecast, 0.01f); + } } diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java index 3cb27451bd57..d9256247b835 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java @@ -89,6 +89,7 @@ import android.os.PowerManagerInternal; import android.os.PowerSaveState; import android.os.UserHandle; import android.os.test.TestLooper; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; @@ -119,6 +120,7 @@ import com.android.server.power.PowerManagerService.WakeLock; import com.android.server.power.batterysaver.BatterySaverController; import com.android.server.power.batterysaver.BatterySaverPolicy; import com.android.server.power.batterysaver.BatterySaverStateMachine; +import com.android.server.power.feature.flags.Flags; import com.android.server.power.feature.PowerManagerFlags; import com.android.server.testutils.OffsettableClock; @@ -3456,6 +3458,25 @@ public class PowerManagerServiceTest { } @Test + @DisableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) + public void testAcquireWakelock_screenTimeoutListenersDisabled_noCrashes() { + IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); + when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds); + + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.displayGroupId = Display.DEFAULT_DISPLAY_GROUP; + when(mDisplayManagerInternalMock.getDisplayInfo(Display.DEFAULT_DISPLAY)) + .thenReturn(displayInfo); + + createService(); + startSystem(); + + acquireWakeLock("screenBright", PowerManager.SCREEN_BRIGHT_WAKE_LOCK, + Display.DEFAULT_DISPLAY); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) public void testAddWakeLockKeepingScreenOn_addsToNotifierAndReportsTimeoutPolicyChange() { IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds); @@ -3483,6 +3504,7 @@ public class PowerManagerServiceTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) public void test_addAndRemoveScreenTimeoutListener_propagatesToNotifier() throws Exception { IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); @@ -3509,6 +3531,7 @@ public class PowerManagerServiceTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) public void test_displayIsRemoved_clearsScreenTimeoutListeners() throws Exception { IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); @@ -3531,7 +3554,8 @@ public class PowerManagerServiceTest { } @Test - public void testScreenWakeLockListener_screenHasWakelocks_addsWithHeldTimeoutPolicyToNotifier() + @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) + public void testScreenTimeoutListener_screenHasWakelocks_addsWithHeldTimeoutPolicyToNotifier() throws Exception { IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds); diff --git a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java index e86108d84538..ede61a5a0269 100644 --- a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java +++ b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java @@ -15,18 +15,14 @@ */ package com.android.server.selinux; -import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; import static com.android.server.selinux.SelinuxAuditLogBuilder.toCategories; import static com.google.common.truth.Truth.assertThat; -import android.provider.DeviceConfig; - import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,24 +41,12 @@ public class SelinuxAuditLogsBuilderTest { @Before public void setUp() { - runWithShellPermissionIdentity( - () -> - DeviceConfig.setLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN, - TEST_DOMAIN)); - - mAuditLogBuilder = new SelinuxAuditLogBuilder(); + mAuditLogBuilder = new SelinuxAuditLogBuilder(TEST_DOMAIN); mScontextMatcher = mAuditLogBuilder.mScontextMatcher; mTcontextMatcher = mAuditLogBuilder.mTcontextMatcher; mPathMatcher = mAuditLogBuilder.mPathMatcher; } - @After - public void tearDown() { - runWithShellPermissionIdentity(() -> DeviceConfig.clearAllLocalOverrides()); - } - @Test public void testMatcher_scontext() { assertThat(mScontextMatcher.reset("u:r:" + TEST_DOMAIN + ":s0").matches()).isTrue(); @@ -109,13 +93,9 @@ public class SelinuxAuditLogsBuilderTest { @Test public void testMatcher_scontextDefaultConfig() { - runWithShellPermissionIdentity( - () -> - DeviceConfig.clearLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN)); - - Matcher scontexMatcher = new SelinuxAuditLogBuilder().mScontextMatcher; + Matcher scontexMatcher = + new SelinuxAuditLogBuilder(SelinuxAuditLogsCollector.DEFAULT_SELINUX_AUDIT_DOMAIN) + .mScontextMatcher; assertThat(scontexMatcher.reset("u:r:" + TEST_DOMAIN + ":s0").matches()).isFalse(); assertThat(scontexMatcher.reset("u:r:" + TEST_DOMAIN + ":s0:c123,c456").matches()) @@ -221,13 +201,7 @@ public class SelinuxAuditLogsBuilderTest { @Test public void testSelinuxAuditLogsBuilder_wrongConfig() { String notARegexDomain = "not]a[regex"; - runWithShellPermissionIdentity( - () -> - DeviceConfig.setLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN, - notARegexDomain)); - SelinuxAuditLogBuilder noOpBuilder = new SelinuxAuditLogBuilder(); + SelinuxAuditLogBuilder noOpBuilder = new SelinuxAuditLogBuilder(notARegexDomain); noOpBuilder.reset( "granted { p } scontext=u:r:" diff --git a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java index b6ccf5e0ad80..db58c74e8431 100644 --- a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java +++ b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java @@ -15,7 +15,6 @@ */ package com.android.server.selinux; -import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -28,7 +27,6 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; -import android.provider.DeviceConfig; import android.util.EventLog; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -59,6 +57,7 @@ public class SelinuxAuditLogsCollectorTest { private final SelinuxAuditLogsCollector mSelinuxAutidLogsCollector = // Ignore rate limiting for tests new SelinuxAuditLogsCollector( + () -> TEST_DOMAIN, new RateLimiter(mClock, /* window= */ Duration.ofMillis(0)), new QuotaLimiter( mClock, /* windowSize= */ Duration.ofHours(1), /* maxPermits= */ 5)); @@ -67,13 +66,6 @@ public class SelinuxAuditLogsCollectorTest { @Before public void setUp() { - runWithShellPermissionIdentity( - () -> - DeviceConfig.setLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN, - TEST_DOMAIN)); - mSelinuxAutidLogsCollector.setStopRequested(false); // move the clock forward for the limiters. mClock.currentTimeMillis += Duration.ofHours(1).toMillis(); @@ -85,7 +77,6 @@ public class SelinuxAuditLogsCollectorTest { @After public void tearDown() { - runWithShellPermissionIdentity(() -> DeviceConfig.clearAllLocalOverrides()); mMockitoSession.finishMocking(); } diff --git a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java index 2ed71cecd79d..952d8fa47a34 100644 --- a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java @@ -118,7 +118,6 @@ public class ThermalManagerServiceTest { private class ThermalHalFake extends ThermalHalWrapper { private static final int INIT_STATUS = Temperature.THROTTLING_NONE; private List<Temperature> mTemperatureList = new ArrayList<>(); - private List<Temperature> mOverrideTemperatures = null; private List<CoolingDevice> mCoolingDeviceList = new ArrayList<>(); private List<TemperatureThreshold> mTemperatureThresholdList = initializeThresholds(); @@ -132,6 +131,9 @@ public class ThermalManagerServiceTest { INIT_STATUS); private CoolingDevice mCpu = new CoolingDevice(40, CoolingDevice.TYPE_BATTERY, "cpu"); private CoolingDevice mGpu = new CoolingDevice(43, CoolingDevice.TYPE_BATTERY, "gpu"); + private Map<Integer, Float> mForecastSkinTemperatures = null; + private int mForecastSkinTemperaturesCalled = 0; + private boolean mForecastSkinTemperaturesError = false; private List<TemperatureThreshold> initializeThresholds() { ArrayList<TemperatureThreshold> thresholds = new ArrayList<>(); @@ -173,12 +175,17 @@ public class ThermalManagerServiceTest { mCoolingDeviceList.add(mGpu); } - void setOverrideTemperatures(List<Temperature> temperatures) { - mOverrideTemperatures = temperatures; + void enableForecastSkinTemperature() { + mForecastSkinTemperatures = Map.of(0, 22.0f, 10, 25.0f, 20, 28.0f, + 30, 31.0f, 40, 34.0f, 50, 37.0f, 60, 40.0f); } - void resetOverrideTemperatures() { - mOverrideTemperatures = null; + void disableForecastSkinTemperature() { + mForecastSkinTemperatures = null; + } + + void failForecastSkinTemperature() { + mForecastSkinTemperaturesError = true; } @Override @@ -219,6 +226,18 @@ public class ThermalManagerServiceTest { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + mForecastSkinTemperaturesCalled++; + if (mForecastSkinTemperaturesError) { + throw new RuntimeException(); + } + if (mForecastSkinTemperatures == null) { + throw new UnsupportedOperationException(); + } + return mForecastSkinTemperatures.get(forecastSeconds); + } + + @Override protected boolean connectToHal() { return true; } @@ -388,7 +407,7 @@ public class ThermalManagerServiceTest { Thread.sleep(CALLBACK_TIMEOUT_MILLI_SEC); resetListenerMock(); int status = Temperature.THROTTLING_SEVERE; - mFakeHal.setOverrideTemperatures(new ArrayList<>()); + mFakeHal.mTemperatureList = new ArrayList<>(); // Should not notify on non-skin type Temperature newBattery = new Temperature(37, Temperature.TYPE_BATTERY, "batt", status); @@ -518,6 +537,99 @@ public class ThermalManagerServiceTest { } @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST}) + public void testGetThermalHeadroom_halForecast() throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertTrue(mService.mIsHalSkinForecastSupported.get()); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.mForecastSkinTemperaturesCalled = 0; + + assertEquals(1.0f, mService.mService.getThermalHeadroom(60), 0.01f); + assertEquals(0.9f, mService.mService.getThermalHeadroom(50), 0.01f); + assertEquals(0.8f, mService.mService.getThermalHeadroom(40), 0.01f); + assertEquals(0.7f, mService.mService.getThermalHeadroom(30), 0.01f); + assertEquals(0.6f, mService.mService.getThermalHeadroom(20), 0.01f); + assertEquals(0.5f, mService.mService.getThermalHeadroom(10), 0.01f); + assertEquals(0.4f, mService.mService.getThermalHeadroom(0), 0.01f); + assertEquals(7, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST}) + public void testGetThermalHeadroom_halForecast_disabledOnMultiThresholds() + throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + List<TemperatureThreshold> thresholds = mFakeHal.initializeThresholds(); + TemperatureThreshold skinThreshold = new TemperatureThreshold(); + skinThreshold.type = Temperature.TYPE_SKIN; + skinThreshold.name = "skin2"; + skinThreshold.hotThrottlingThresholds = new float[7 /*ThrottlingSeverity#len*/]; + skinThreshold.coldThrottlingThresholds = new float[7 /*ThrottlingSeverity#len*/]; + for (int i = 0; i < skinThreshold.hotThrottlingThresholds.length; ++i) { + // Sets NONE to 45.0f, SEVERE to 60.0f, and SHUTDOWN to 75.0f + skinThreshold.hotThrottlingThresholds[i] = 45.0f + 5.0f * i; + } + thresholds.add(skinThreshold); + mFakeHal.mTemperatureThresholdList = thresholds; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertFalse("HAL skin forecast should be disabled on multiple SKIN thresholds", + mService.mIsHalSkinForecastSupported.get()); + mService.mService.getThermalHeadroom(10); + assertEquals(0, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST, + Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK}) + public void testGetThermalHeadroom_halForecast_disabledOnMultiThresholdsCallback() + throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertTrue(mService.mIsHalSkinForecastSupported.get()); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.mForecastSkinTemperaturesCalled = 0; + + TemperatureThreshold newThreshold = new TemperatureThreshold(); + newThreshold.name = "skin2"; + newThreshold.type = Temperature.TYPE_SKIN; + newThreshold.hotThrottlingThresholds = new float[]{ + Float.NaN, 43.0f, 46.0f, 49.0f, Float.NaN, Float.NaN, Float.NaN + }; + mFakeHal.mCallback.onThresholdChanged(newThreshold); + mService.mService.getThermalHeadroom(10); + assertEquals(0, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST}) + public void testGetThermalHeadroom_halForecast_errorOnHal() throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertTrue(mService.mIsHalSkinForecastSupported.get()); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.mForecastSkinTemperaturesCalled = 0; + + mFakeHal.disableForecastSkinTemperature(); + assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(10))); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.enableForecastSkinTemperature(); + assertFalse(Float.isNaN(mService.mService.getThermalHeadroom(10))); + assertEquals(2, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.failForecastSkinTemperature(); + assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(10))); + assertEquals(3, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test @EnableFlags({Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK, Flags.FLAG_ALLOW_THERMAL_HEADROOM_THRESHOLDS}) public void testTemperatureWatcherUpdateSevereThresholds() throws Exception { diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java b/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java new file mode 100644 index 000000000000..23c13bd04368 --- /dev/null +++ b/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java @@ -0,0 +1,527 @@ +/* + * Copyright 2024 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.timezonedetector; + +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.AUTO_REVERT_THRESHOLD; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.SIGNAL_TYPE_NONE; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.SIGNAL_TYPE_UNKNOWN; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_REJECTED; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_SUPERSEDED; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_UNKNOWN; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_UNTRACKED; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.spy; + +import android.annotation.Nullable; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.UiAutomation; +import android.content.Context; +import android.os.HandlerThread; +import android.os.Process; +import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.flags.Flags; +import com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.TimeZoneChangeRecord; +import com.android.server.timezonedetector.TimeZoneChangeListener.TimeZoneChangeEvent; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.time.InstantSource; +import java.util.ArrayList; +import java.util.List; + +/** + * White-box unit tests for {@link NotifyingTimeZoneChangeListener}. + */ +@RunWith(JUnitParamsRunner.class) +@EnableFlags(Flags.FLAG_DATETIME_NOTIFICATIONS) +public class NotifyingTimeZoneChangeListenerTest { + + @ClassRule + public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); + + @Rule(order = 0) + public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); + + @Rule(order = 1) + public final MockitoRule mockito = MockitoJUnit.rule(); + + public static List<@TimeZoneDetectorStrategy.Origin Integer> getDetectionOrigins() { + return List.of(ORIGIN_LOCATION, ORIGIN_TELEPHONY); + } + + private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234; + /** A time zone used for initialization that does not occur elsewhere in tests. */ + private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC"; + private static final String INTERACT_ACROSS_USERS_FULL_PERMISSION = + "android.permission.INTERACT_ACROSS_USERS_FULL"; + + @Mock + private Context mContext; + private UiAutomation mUiAutomation; + + private FakeNotificationManager mNotificationManager; + private HandlerThread mHandlerThread; + private TestHandler mHandler; + private FakeServiceConfigAccessor mServiceConfigAccessor; + private int mUid; + + private NotifyingTimeZoneChangeListener mTimeZoneChangeTracker; + + @Before + public void setUp() { + mUid = Process.myUid(); + // Create a thread + handler for processing the work that the service posts. + mHandlerThread = new HandlerThread("TimeZoneDetectorInternalTest"); + mHandlerThread.start(); + mHandler = new TestHandler(mHandlerThread.getLooper()); + + ConfigurationInternal config = new ConfigurationInternal.Builder() + .setUserId(mUid) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(false) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .setNotificationsSupported(true) + .setNotificationsTrackingSupported(true) + .setNotificationsEnabledSetting(false) + .setManualChangeTrackingSupported(false) + .build(); + + mServiceConfigAccessor = spy(new FakeServiceConfigAccessor()); + mServiceConfigAccessor.initializeCurrentUserConfiguration(config); + + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); + mUiAutomation.adoptShellPermissionIdentity(INTERACT_ACROSS_USERS_FULL_PERMISSION); + + mNotificationManager = new FakeNotificationManager(mContext, InstantSource.system()); + + mTimeZoneChangeTracker = new NotifyingTimeZoneChangeListener(mHandler, mContext, + mServiceConfigAccessor, mNotificationManager); + } + + @After + public void tearDown() throws Exception { + mHandlerThread.quit(); + mHandlerThread.join(); + } + + @Test + public void process_autoDetectionOff_noManualTracking_shouldTrackWithoutNotifying() { + enableTimeZoneNotifications(); + + TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ ORIGIN_MANUAL, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + + assertEquals(expectedChangeEvent, mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + assertEquals(0, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(0); + } + + @Test + public void process_autoDetectionOff_shouldTrackWithoutNotifying() { + enableNotificationsWithManualChangeTracking(); + + TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ ORIGIN_MANUAL, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + + assertEquals(expectedChangeEvent, mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + assertEquals(0, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(1); + } + + @Test + @Parameters(method = "getDetectionOrigins") + public void process_automaticDetection_trackingSupported( + @TimeZoneDetectorStrategy.Origin int origin) { + if (origin == ORIGIN_LOCATION) { + enableLocationTimeZoneDetection(); + } else if (origin == ORIGIN_TELEPHONY) { + enableTelephonyTimeZoneDetection(); + } else { + throw new IllegalStateException( + "The given origin has not been implemented for this test: " + origin); + } + + enableNotificationsWithManualChangeTracking(); + + TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + // lastTrackedChangeEvent == null + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + TimeZoneChangeRecord trackedEvent1 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(expectedChangeEvent, trackedEvent1); + assertEquals(1, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(1); + + expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 2, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 1000L, + /* unixEpochTimeMillis= */ 1726597800000L + 1000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/London", + /* newZoneId= */ "Europe/Paris", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + // lastTrackedChangeEvent != null + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + TimeZoneChangeRecord trackedEvent2 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(STATUS_SUPERSEDED, trackedEvent1.getStatus()); + assertEquals(expectedChangeEvent, trackedEvent2); + assertEquals(2, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(2); + + disableTimeZoneAutoDetection(); + + // Test manual change within revert threshold + { + expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 3, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 999L + AUTO_REVERT_THRESHOLD, + /* unixEpochTimeMillis= */ + 1726597800000L + 999L + AUTO_REVERT_THRESHOLD, + /* origin= */ ORIGIN_MANUAL, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + TimeZoneChangeRecord trackedEvent3 = + mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + // The user manually changed the time zone within a short period of receiving the + // notification, indicating that they rejected the automatic change of time zone + assertEquals(STATUS_REJECTED, trackedEvent2.getStatus()); + assertEquals(expectedChangeEvent, trackedEvent3); + assertEquals(2, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(3); + } + + // Test manual change outside of revert threshold + { + // [START] Reset previous event + enableNotificationsWithManualChangeTracking(); + mTimeZoneChangeTracker.process(trackedEvent2.getEvent()); + trackedEvent2 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + disableTimeZoneAutoDetection(); + // [END] Reset previous event + + expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 5, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 1001L + AUTO_REVERT_THRESHOLD, + /* unixEpochTimeMillis= */ + 1726597800000L + 1001L + AUTO_REVERT_THRESHOLD, + /* origin= */ ORIGIN_MANUAL, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + TimeZoneChangeRecord trackedEvent3 = + mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + // The user manually changed the time zone outside of the period we consider as a revert + assertEquals(STATUS_SUPERSEDED, trackedEvent2.getStatus()); + assertEquals(expectedChangeEvent, trackedEvent3); + assertEquals(3, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(5); + } + } + + @Test + @Parameters(method = "getDetectionOrigins") + public void process_automaticDetection_trackingSupported_missingTransition( + @TimeZoneDetectorStrategy.Origin int origin) { + if (origin == ORIGIN_LOCATION) { + enableLocationTimeZoneDetection(); + } else if (origin == ORIGIN_TELEPHONY) { + enableTelephonyTimeZoneDetection(); + } else { + throw new IllegalStateException( + "The given origin has not been implemented for this test: " + origin); + } + + enableNotificationsWithManualChangeTracking(); + + TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + // lastTrackedChangeEvent == null + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + TimeZoneChangeRecord trackedEvent1 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(expectedChangeEvent, trackedEvent1); + assertEquals(1, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(1); + + expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 3, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 1000L, + /* unixEpochTimeMillis= */ 1726597800000L + 1000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Athens", + /* newZoneId= */ "Europe/Paris", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + // lastTrackedChangeEvent != null + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + TimeZoneChangeRecord trackedEvent2 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(STATUS_SUPERSEDED, trackedEvent1.getStatus()); + assertEquals(expectedChangeEvent, trackedEvent2); + assertEquals(2, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(2); + } + + @Test + @Parameters(method = "getDetectionOrigins") + public void process_automaticDetection_trackingSupported_sameOffset( + @TimeZoneDetectorStrategy.Origin int origin) { + if (origin == ORIGIN_LOCATION) { + enableLocationTimeZoneDetection(); + } else if (origin == ORIGIN_TELEPHONY) { + enableTelephonyTimeZoneDetection(); + } else { + throw new IllegalStateException( + "The given origin has not been implemented for this test: " + origin); + } + + enableNotificationsWithManualChangeTracking(); + + TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/Rome", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + // lastTrackedChangeEvent == null + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + TimeZoneChangeRecord trackedEvent1 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(expectedChangeEvent, trackedEvent1); + // No notification sent for the same UTC offset + assertEquals(0, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(1); + } + + private void enableLocationTimeZoneDetection() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setAutoDetectionEnabledSetting(true) + .setGeoDetectionFeatureSupported(true) + .setGeoDetectionEnabledSetting(true) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private void enableTelephonyTimeZoneDetection() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setAutoDetectionEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .setTelephonyDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(true) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private void enableTimeZoneNotifications() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setNotificationsSupported(true) + .setNotificationsTrackingSupported(true) + .setNotificationsEnabledSetting(true) + .setManualChangeTrackingSupported(false) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private void enableNotificationsWithManualChangeTracking() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setNotificationsSupported(true) + .setNotificationsTrackingSupported(true) + .setNotificationsEnabledSetting(true) + .setManualChangeTrackingSupported(true) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private void disableTimeZoneAutoDetection() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setAutoDetectionEnabledSetting(false) + .setGeoDetectionEnabledSetting(false) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private ConfigurationInternal.Builder toBuilder(ConfigurationInternal config) { + return new ConfigurationInternal.Builder() + .setUserId(config.getUserId()) + .setTelephonyDetectionFeatureSupported(config.isTelephonyDetectionSupported()) + .setGeoDetectionFeatureSupported(config.isGeoDetectionSupported()) + .setTelephonyFallbackSupported(config.isTelephonyFallbackSupported()) + .setGeoDetectionRunInBackgroundEnabled( + config.getGeoDetectionRunInBackgroundEnabledSetting()) + .setEnhancedMetricsCollectionEnabled(config.isEnhancedMetricsCollectionEnabled()) + .setUserConfigAllowed(config.isUserConfigAllowed()) + .setAutoDetectionEnabledSetting(config.getAutoDetectionEnabledSetting()) + .setLocationEnabledSetting(config.getLocationEnabledSetting()) + .setGeoDetectionEnabledSetting(config.getGeoDetectionEnabledSetting()) + .setNotificationsTrackingSupported(config.isNotificationTrackingSupported()) + .setNotificationsEnabledSetting(config.getNotificationsEnabledBehavior()) + .setNotificationsSupported(config.areNotificationsSupported()) + .setManualChangeTrackingSupported(config.isManualChangeTrackingSupported()); + } + + private static class FakeNotificationManager extends NotificationManager { + + private final List<Notification> mNotifications = new ArrayList<>(); + + FakeNotificationManager(Context context, InstantSource clock) { + super(context, clock); + } + + @Override + public void notifyAsUser(@Nullable String tag, int id, Notification notification, + UserHandle user) { + mNotifications.add(notification); + } + + public List<Notification> getNotifications() { + return mNotifications; + } + } +} diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java index 47a9b2c47173..9a01fa2eb966 100644 --- a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java +++ b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java @@ -41,6 +41,9 @@ import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_ import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_ENABLED_GEO_ENABLED; import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_USER_RESTRICTED_AUTO_ENABLED; import static com.android.server.timezonedetector.ConfigInternalForTests.USER_ID; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGH; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGHEST; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_LOW; @@ -73,16 +76,21 @@ import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion.MatchType; import android.app.timezonedetector.TelephonyTimeZoneSuggestion.Quality; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.service.timezone.TimeZoneProviderStatus; import android.util.IndentingPrintWriter; import com.android.server.SystemTimeZone.TimeZoneConfidence; +import com.android.server.flags.Flags; import com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.QualifiedTelephonyTimeZoneSuggestion; import junitparams.JUnitParamsRunner; import junitparams.Parameters; import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -98,8 +106,14 @@ import java.util.function.Function; * White-box unit tests for {@link TimeZoneDetectorStrategyImpl}. */ @RunWith(JUnitParamsRunner.class) +@EnableFlags(Flags.FLAG_DATETIME_NOTIFICATIONS) public class TimeZoneDetectorStrategyImplTest { + @ClassRule + public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); + @Rule + public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); + private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234; /** A time zone used for initialization that does not occur elsewhere in tests. */ private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC"; @@ -130,6 +144,7 @@ public class TimeZoneDetectorStrategyImplTest { private FakeServiceConfigAccessor mFakeServiceConfigAccessorSpy; private FakeEnvironment mFakeEnvironment; + private FakeTimeZoneChangeEventListener mFakeTimeZoneChangeEventTracker; private TimeZoneDetectorStrategyImpl mTimeZoneDetectorStrategy; @@ -139,9 +154,10 @@ public class TimeZoneDetectorStrategyImplTest { mFakeServiceConfigAccessorSpy = spy(new FakeServiceConfigAccessor()); mFakeServiceConfigAccessorSpy.initializeCurrentUserConfiguration( CONFIG_AUTO_DISABLED_GEO_DISABLED); + mFakeTimeZoneChangeEventTracker = new FakeTimeZoneChangeEventListener(); mTimeZoneDetectorStrategy = new TimeZoneDetectorStrategyImpl( - mFakeServiceConfigAccessorSpy, mFakeEnvironment); + mFakeServiceConfigAccessorSpy, mFakeEnvironment, mFakeTimeZoneChangeEventTracker); } @Test @@ -363,6 +379,10 @@ public class TimeZoneDetectorStrategyImplTest { // SlotIndex1 should always beat slotIndex2, all other things being equal. assertEquals(expectedSlotIndex1ScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } /** @@ -398,6 +418,10 @@ public class TimeZoneDetectorStrategyImplTest { SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } // A good quality suggestion will be used. @@ -415,6 +439,13 @@ public class TimeZoneDetectorStrategyImplTest { SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); + + if (Flags.datetimeNotifications()) { + assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + assertEquals(ORIGIN_TELEPHONY, + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().getFirst() + .getOrigin()); + } } // A low quality suggestion will be accepted, but not used to set the device time zone. @@ -432,6 +463,11 @@ public class TimeZoneDetectorStrategyImplTest { SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); + + if (Flags.datetimeNotifications()) { + // Still 1 from last good quality suggestion but not recorded as quality is too low + assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } } @@ -492,6 +528,17 @@ public class TimeZoneDetectorStrategyImplTest { assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); } + + if (Flags.datetimeNotifications()) { + /* + * Only 6 out of 7 tests have a quality good enough to trigger an event and the + * configuration is reset at every loop. + */ + assertEquals(6, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + assertEquals(ORIGIN_TELEPHONY, + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().getFirst() + .getOrigin()); + } } @Test @@ -518,6 +565,18 @@ public class TimeZoneDetectorStrategyImplTest { for (TelephonyTestCase testCase : descendingCasesByScore) { makeSlotIndex1SuggestionAndCheckState(script, testCase); } + + if (Flags.datetimeNotifications()) { + /* + * Only 6 out of 7 tests have a quality good enough to trigger an event and the + * set of tests is run twice. + */ + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(12, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.getFirst().getOrigin()); + } } private void makeSlotIndex1SuggestionAndCheckState(Script script, TelephonyTestCase testCase) { @@ -641,6 +700,18 @@ public class TimeZoneDetectorStrategyImplTest { .verifyLatestQualifiedTelephonySuggestionReceived( SLOT_INDEX2, expectedEmptySlotIndex2ScoredSuggestion); } + + if (Flags.datetimeNotifications()) { + /* + * Only 6 out of 7 tests have a quality good enough to trigger an event and the + * simulation runs twice per loop with a different time zone (i.e. London and Paris). + */ + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(12, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.getFirst().getOrigin()); + } } /** @@ -683,6 +754,14 @@ public class TimeZoneDetectorStrategyImplTest { // Latest suggestion should be used. script.simulateSetAutoMode(true) .verifyTimeZoneChangedAndReset(newYorkSuggestion); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(2, timeZoneChangeEvents.size()); + assertTrue(timeZoneChangeEvents.stream() + .allMatch(x -> x.getOrigin() == ORIGIN_TELEPHONY)); + } } @Test @@ -714,6 +793,10 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneNotChanged(); assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion()); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } @Test @@ -732,6 +815,15 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneChangedAndReset(manualSuggestion); assertEquals(manualSuggestion, mTimeZoneDetectorStrategy.getLatestManualSuggestion()); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin() + ); + } } @Test @@ -754,6 +846,10 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneNotChanged(); assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion()); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } @Test @@ -780,6 +876,16 @@ public class TimeZoneDetectorStrategyImplTest { script.verifyTimeZoneNotChanged(); assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion()); } + + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + if (Flags.datetimeNotifications() && expectedResult) { + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin()); + } else { + assertEmpty(timeZoneChangeEvents); + } } @Test @@ -830,6 +936,10 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } { @@ -857,6 +967,10 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } } @@ -893,6 +1007,10 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } @Test @@ -927,6 +1045,10 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } @Test @@ -962,6 +1084,14 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.getFirst().getOrigin()); + } } /** @@ -999,6 +1129,17 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(londonOrParisEvent) .verifyTimeZoneNotChanged() .verifyLatestLocationAlgorithmEventReceived(londonOrParisEvent); + + if (Flags.datetimeNotifications()) { + // we do not record events if the time zone does not change (i.e. 2 / 4 of the + // simulated cases) + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(2, timeZoneChangeEvents.size()); + assertTrue(timeZoneChangeEvents.stream() + .allMatch(x -> x.getOrigin() == ORIGIN_LOCATION)); + } } /** @@ -1059,6 +1200,16 @@ public class TimeZoneDetectorStrategyImplTest { // A configuration change is considered a "state change". assertStateChangeNotificationsSent(stateChangeListener, 1); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(3, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(2).getOrigin()); + } } @Test @@ -1088,6 +1239,14 @@ public class TimeZoneDetectorStrategyImplTest { .simulateTelephonyTimeZoneSuggestion(telephonySuggestion) .verifyTimeZoneChangedAndReset(telephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin()); + } } // Receiving an "uncertain" geolocation suggestion should have no effect. @@ -1098,6 +1257,11 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Receiving a "certain" geolocation suggestion should disable telephony fallback mode. @@ -1109,6 +1273,14 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(2, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin()); + } } // Used to record the last telephony suggestion received, which will be used when fallback @@ -1125,6 +1297,11 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); lastTelephonySuggestion = telephonySuggestion; + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(2, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Geolocation suggestions should continue to be used as normal (previous telephony @@ -1151,6 +1328,14 @@ public class TimeZoneDetectorStrategyImplTest { // No change needed, device will already be set to Europe/Rome. .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(3, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(2).getOrigin()); + } } // Enable telephony fallback. Nothing will change, because the geolocation is still certain, @@ -1160,6 +1345,11 @@ public class TimeZoneDetectorStrategyImplTest { .simulateEnableTelephonyFallback() .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(3, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Make the geolocation algorithm uncertain. @@ -1170,6 +1360,14 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(lastTelephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(4, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(3).getOrigin()); + } } // Make the geolocation algorithm certain, disabling telephony fallback. @@ -1181,6 +1379,14 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(5, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(4).getOrigin()); + } } // Demonstrate what happens when geolocation is uncertain when telephony fallback is @@ -1195,6 +1401,14 @@ public class TimeZoneDetectorStrategyImplTest { .simulateEnableTelephonyFallback() .verifyTimeZoneChangedAndReset(lastTelephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(6, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(5).getOrigin()); + } } } @@ -1225,6 +1439,13 @@ public class TimeZoneDetectorStrategyImplTest { .simulateTelephonyTimeZoneSuggestion(telephonySuggestion) .verifyTimeZoneChangedAndReset(telephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin()); + } } // Receiving an "uncertain" geolocation suggestion without a status should have no effect. @@ -1235,6 +1456,11 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Receiving a "certain" geolocation suggestion should disable telephony fallback mode. @@ -1246,6 +1472,13 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(2, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin()); + } } // Used to record the last telephony suggestion received, which will be used when fallback @@ -1262,6 +1495,11 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); lastTelephonySuggestion = telephonySuggestion; + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(2, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Geolocation suggestions should continue to be used as normal (previous telephony @@ -1291,6 +1529,13 @@ public class TimeZoneDetectorStrategyImplTest { // No change needed, device will already be set to Europe/Rome. .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(3, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(2).getOrigin()); + } } // Enable telephony fallback via a LocationAlgorithmEvent containing an "uncertain" @@ -1310,6 +1555,13 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(uncertainEventBlockedBySettings) .verifyTimeZoneChangedAndReset(lastTelephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(4, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(3).getOrigin()); + } } // Make the geolocation algorithm certain, disabling telephony fallback. @@ -1321,6 +1573,13 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(5, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(4).getOrigin()); + } } } @@ -1349,6 +1608,10 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } // Make an uncertain geolocation suggestion, there is no telephony suggestion to fall back @@ -1360,6 +1623,10 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } // Similar to the case above, but force a fallback attempt after making a "certain" @@ -1386,6 +1653,13 @@ public class TimeZoneDetectorStrategyImplTest { .simulateEnableTelephonyFallback() .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(0).getOrigin()); + } } } @@ -1803,6 +2077,16 @@ public class TimeZoneDetectorStrategyImplTest { userId, manualTimeZoneSuggestion, bypassUserPolicyChecks); assertEquals(expectedResult, actualResult); + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + if (actualResult && Flags.datetimeNotifications()) { + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin()); + } else { + assertEmpty(timeZoneChangeEvents); + } + return this; } diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java index 23dcb65eb30f..dc16de1aab5e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java @@ -43,6 +43,7 @@ import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -92,6 +93,7 @@ import org.mockito.Mockito; import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; /** * Tests for the {@link DragDropController} class. @@ -255,7 +257,7 @@ public class DragDropControllerTests extends WindowTestsBase { iwindow.setDragEventJournal(dragEvents); startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { // Verify the start-drag event is sent for invisible windows final DragEvent dragEvent = dragEvents.get(0); assertTrue(dragEvent.getAction() == ACTION_DRAG_STARTED); @@ -297,7 +299,7 @@ public class DragDropControllerTests extends WindowTestsBase { globalInterceptIWindow.setDragEventJournal(globalInterceptWindowDragEvents); startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, - createClipDataForActivity(null, mock(UserHandle.class)), () -> { + createClipDataForActivity(null, mock(UserHandle.class)), (unused) -> { // Verify the start-drag event is sent for the local and global intercept window // but not the other window assertTrue(nonLocalWindowDragEvents.isEmpty()); @@ -340,7 +342,7 @@ public class DragDropControllerTests extends WindowTestsBase { iwindow.setDragEventJournal(dragEvents); startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { // Verify the start-drag event has the drag flags final DragEvent dragEvent = dragEvents.get(0); assertTrue(dragEvent.getAction() == ACTION_DRAG_STARTED); @@ -386,7 +388,7 @@ public class DragDropControllerTests extends WindowTestsBase { iwindow2.setDragEventJournal(dragEvents2); startDrag(dragStartX, dragStartY, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { // Verify the start-drag event is sent as-is for the drag origin window. final DragEvent dragEvent = dragEvents.get(0); assertEquals(ACTION_DRAG_STARTED, dragEvent.getAction()); @@ -413,8 +415,14 @@ public class DragDropControllerTests extends WindowTestsBase { assertEquals(ACTION_DROP, dropEvent.getAction()); assertEquals(dropCoordsPx, dropEvent.getX(), 0.0 /* delta */); assertEquals(dropCoordsPx, dropEvent.getY(), 0.0 /* delta */); + assertEquals(window2.getDisplayId(), dropEvent.getDisplayId()); mTarget.reportDropResult(iwindow2, true); + // Verify both windows received ACTION_DRAG_ENDED event. + assertEquals(ACTION_DRAG_ENDED, last(dragEvents).getAction()); + assertEquals(window2.getDisplayId(), last(dragEvents).getDisplayId()); + assertEquals(ACTION_DRAG_ENDED, last(dragEvents2).getAction()); + assertEquals(window2.getDisplayId(), last(dragEvents2).getDisplayId()); } finally { mTarget.mDeferDragStateClosed = false; } @@ -441,7 +449,7 @@ public class DragDropControllerTests extends WindowTestsBase { iwindow2.setDragEventJournal(dragEvents2); startDrag(dragStartX, dragStartY, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { // Verify the start-drag event is sent as-is for the drag origin window. final DragEvent dragEvent = dragEvents.get(0); assertEquals(ACTION_DRAG_STARTED, dragEvent.getAction()); @@ -456,10 +464,12 @@ public class DragDropControllerTests extends WindowTestsBase { try { mTarget.mDeferDragStateClosed = true; + mTarget.handleMotionEvent(true, testDisplay.getDisplayId(), dropCoordsPx, + dropCoordsPx); // x, y is window-local coordinate. mTarget.reportDropWindow(window2.mInputChannelToken, dropCoordsPx, dropCoordsPx); - mTarget.handleMotionEvent(false, window2.getDisplayId(), dropCoordsPx, + mTarget.handleMotionEvent(false, testDisplay.getDisplayId(), dropCoordsPx, dropCoordsPx); mToken = window2.mClient.asBinder(); // Verify only window2 received the DROP event and coords are sent as-is @@ -469,14 +479,70 @@ public class DragDropControllerTests extends WindowTestsBase { assertEquals(ACTION_DROP, dropEvent.getAction()); assertEquals(dropCoordsPx, dropEvent.getX(), 0.0 /* delta */); assertEquals(dropCoordsPx, dropEvent.getY(), 0.0 /* delta */); + assertEquals(testDisplay.getDisplayId(), dropEvent.getDisplayId()); mTarget.reportDropResult(iwindow2, true); + // Verify both windows received ACTION_DRAG_ENDED event. + assertEquals(ACTION_DRAG_ENDED, last(dragEvents).getAction()); + assertEquals(testDisplay.getDisplayId(), last(dragEvents).getDisplayId()); + assertEquals(ACTION_DRAG_ENDED, last(dragEvents2).getAction()); + assertEquals(testDisplay.getDisplayId(), last(dragEvents2).getDisplayId()); } finally { mTarget.mDeferDragStateClosed = false; } }); } + @Test + public void testDragMove() { + startDrag(0, 0, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, + ClipData.newPlainText("label", "text"), (surface) -> { + int dragMoveX = mWindow.getBounds().centerX(); + int dragMoveY = mWindow.getBounds().centerY(); + final SurfaceControl.Transaction transaction = + mSystemServicesTestRule.mTransaction; + clearInvocations(transaction); + + mTarget.handleMotionEvent(true, mWindow.getDisplayId(), dragMoveX, dragMoveY); + verify(transaction).setPosition(surface, dragMoveX, dragMoveY); + + // Clean-up. + mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0); + mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), 0, + 0); + mToken = mWindow.mClient.asBinder(); + }); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_DND) + public void testConnectedDisplaysDragMoveToOtherDisplay() { + final float testDensityMultiplier = 1.5f; + final DisplayContent testDisplay = createMockSimulatedDisplay(); + testDisplay.mBaseDisplayDensity = + (int) (mDisplayContent.mBaseDisplayDensity * testDensityMultiplier); + WindowState testWindow = createDropTargetWindow("App drag test window", testDisplay); + + // Test starts from mWindow which is on default display. + startDrag(0, 0, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, + ClipData.newPlainText("label", "text"), (surface) -> { + final SurfaceControl.Transaction transaction = + mSystemServicesTestRule.mTransaction; + clearInvocations(transaction); + mTarget.handleMotionEvent(true, testWindow.getDisplayId(), 0, 0); + + verify(transaction).reparent(surface, testDisplay.getSurfaceControl()); + verify(transaction).setScale(surface, testDensityMultiplier, + testDensityMultiplier); + + // Clean-up. + mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0); + mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), 0, + 0); + mToken = mWindow.mClient.asBinder(); + }); + } + private DragEvent last(ArrayList<DragEvent> list) { return list.get(list.size() - 1); } @@ -645,7 +711,7 @@ public class DragDropControllerTests extends WindowTestsBase { startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { assertTrue(dragEvents.get(0).getAction() == ACTION_DRAG_STARTED); // Verify after consuming that the drag surface is relinquished @@ -676,7 +742,7 @@ public class DragDropControllerTests extends WindowTestsBase { startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { assertTrue(dragEvents.get(0).getAction() == ACTION_DRAG_STARTED); // Verify after consuming that the drag surface is relinquished @@ -713,7 +779,7 @@ public class DragDropControllerTests extends WindowTestsBase { mTarget.setGlobalDragListener(listener); final int invalidXY = 100_000; startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, - ClipData.newPlainText("label", "Test"), () -> { + ClipData.newPlainText("label", "Test"), (unused) -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY); mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), @@ -738,7 +804,7 @@ public class DragDropControllerTests extends WindowTestsBase { mTarget.setGlobalDragListener(listener); final int invalidXY = 100_000; startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, - ClipData.newPlainText("label", "Test"), () -> { + ClipData.newPlainText("label", "Test"), (unused) -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), @@ -761,7 +827,7 @@ public class DragDropControllerTests extends WindowTestsBase { doReturn(mock(Binder.class)).when(listener).asBinder(); mTarget.setGlobalDragListener(listener); final int invalidXY = 100_000; - startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> { + startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), (unused) -> { // Trigger an unhandled drop and verify the global drag listener was not called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); mTarget.handleMotionEvent(false /* keepHandling */, mDisplayContent.getDisplayId(), @@ -784,7 +850,7 @@ public class DragDropControllerTests extends WindowTestsBase { mTarget.setGlobalDragListener(listener); final int invalidXY = 100_000; startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, - ClipData.newPlainText("label", "Test"), () -> { + ClipData.newPlainText("label", "Test"), (unused) -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); mTarget.handleMotionEvent(false /* keepHandling */, @@ -805,7 +871,7 @@ public class DragDropControllerTests extends WindowTestsBase { } private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) { - startDrag(flags, data, () -> { + startDrag(flags, data, (unused) -> { mTarget.reportDropWindow(mWindow.mInputChannelToken, dropX, dropY); mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), dropX, dropY); @@ -816,27 +882,26 @@ public class DragDropControllerTests extends WindowTestsBase { /** * Starts a drag with the given parameters, calls Runnable `r` after drag is started. */ - private void startDrag(int flag, ClipData data, Runnable r) { - startDrag(0, 0, flag, data, r); + private void startDrag(int flag, ClipData data, Consumer<SurfaceControl> c) { + startDrag(0, 0, flag, data, c); } /** * Starts a drag with the given parameters, calls Runnable `r` after drag is started. */ private void startDrag(float startInWindowX, float startInWindowY, int flag, ClipData data, - Runnable r) { + Consumer<SurfaceControl> c) { final SurfaceSession appSession = new SurfaceSession(); try { final SurfaceControl surface = new SurfaceControl.Builder(appSession).setName( "drag surface").setBufferSize(100, 100).setFormat( PixelFormat.TRANSLUCENT).build(); - assertTrue(mWm.mInputManager.startDragAndDrop(new Binder(), new Binder())); mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, flag, surface, 0, 0, 0, startInWindowX, startInWindowY, 0, 0, data); assertNotNull(mToken); - r.run(); + c.accept(surface); } finally { appSession.kill(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java index 5ed2df30518b..cc447a18758c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java @@ -1269,6 +1269,7 @@ public class WindowContainerTests extends WindowTestsBase { final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); spyOn(container); spyOn(surfaceAnimator); + doReturn(t).when(container).getSyncTransaction(); // Trigger for first relative layer call. container.assignRelativeLayer(t, relativeParent, 1 /* layer */); @@ -1295,6 +1296,7 @@ public class WindowContainerTests extends WindowTestsBase { spyOn(container); spyOn(surfaceAnimator); spyOn(surfaceFreezer); + doReturn(t).when(container).getSyncTransaction(); container.setLayer(t, 1); container.setRelativeLayer(t, relativeParent, 2); diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp index 20315561cceb..f00a6cad6b46 100644 --- a/tools/aapt2/cmd/Command.cpp +++ b/tools/aapt2/cmd/Command.cpp @@ -54,7 +54,9 @@ std::string GetSafePath(StringPiece arg) { void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::string* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + if (value) { + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + } return true; }; @@ -65,7 +67,9 @@ void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::st void Command::AddRequiredFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + if (value) { + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + } return true; }; @@ -76,7 +80,9 @@ void Command::AddRequiredFlagList(StringPiece name, StringPiece description, void Command::AddOptionalFlag(StringPiece name, StringPiece description, std::optional<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + if (value) { + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + } return true; }; @@ -87,7 +93,9 @@ void Command::AddOptionalFlag(StringPiece name, StringPiece description, void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + if (value) { + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + } return true; }; @@ -98,7 +106,9 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description, void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::unordered_set<std::string>* value) { auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { - value->emplace(arg); + if (value) { + value->emplace(arg); + } return true; }; @@ -108,7 +118,9 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description, void Command::AddOptionalSwitch(StringPiece name, StringPiece description, bool* value) { auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { - *value = true; + if (value) { + *value = true; + } return true; }; diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp index 2a3cb2a0c65d..ad167c979662 100644 --- a/tools/aapt2/cmd/Command_test.cpp +++ b/tools/aapt2/cmd/Command_test.cpp @@ -159,4 +159,22 @@ TEST(CommandTest, ShortOptions) { ASSERT_NE(0, command.Execute({"-w"s, "2"s}, &std::cerr)); } +TEST(CommandTest, OptionsWithNullptrToAcceptValues) { + TestCommand command; + command.AddRequiredFlag("--rflag", "", nullptr); + command.AddRequiredFlagList("--rlflag", "", nullptr); + command.AddOptionalFlag("--oflag", "", nullptr); + command.AddOptionalFlagList("--olflag", "", (std::vector<std::string>*)nullptr); + command.AddOptionalFlagList("--olflag2", "", (std::unordered_set<std::string>*)nullptr); + command.AddOptionalSwitch("--switch", "", nullptr); + + ASSERT_EQ(0, command.Execute({ + "--rflag"s, "1"s, + "--rlflag"s, "1"s, + "--oflag"s, "1"s, + "--olflag"s, "1"s, + "--olflag2"s, "1"s, + "--switch"s}, &std::cerr)); +} + } // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index 6c3eae11eab9..060bc5fa2242 100644 --- a/tools/aapt2/cmd/Convert.cpp +++ b/tools/aapt2/cmd/Convert.cpp @@ -425,9 +425,6 @@ int ConvertCommand::Action(const std::vector<std::string>& args) { << output_format_.value()); return 1; } - if (enable_sparse_encoding_) { - table_flattener_options_.sparse_entries = SparseEntriesMode::Enabled; - } if (force_sparse_encoding_) { table_flattener_options_.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h index 9452e588953e..98c8f5ff89c0 100644 --- a/tools/aapt2/cmd/Convert.h +++ b/tools/aapt2/cmd/Convert.h @@ -36,11 +36,9 @@ class ConvertCommand : public Command { kOutputFormatProto, kOutputFormatBinary, kOutputFormatBinary), &output_format_); AddOptionalSwitch( "--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.\n" - "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " - "the APK is O+", - &enable_sparse_encoding_); + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" @@ -87,7 +85,6 @@ class ConvertCommand : public Command { std::string output_path_; std::optional<std::string> output_format_; bool verbose_ = false; - bool enable_sparse_encoding_ = false; bool force_sparse_encoding_ = false; bool enable_compact_entries_ = false; std::optional<std::string> resources_config_path_; diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 232b4024abd2..eb71189ffc46 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -2504,9 +2504,6 @@ int LinkCommand::Action(const std::vector<std::string>& args) { << "the --merge-only flag can be only used when building a static library"); return 1; } - if (options_.use_sparse_encoding) { - options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; - } // The default build type. context.SetPackageType(PackageType::kApp); diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index 2f17853718ec..b5bd905c02be 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -75,7 +75,6 @@ struct LinkOptions { bool no_resource_removal = false; bool no_xml_namespaces = false; bool do_not_compress_anything = false; - bool use_sparse_encoding = false; std::unordered_set<std::string> extensions_to_not_compress; std::optional<std::regex> regex_to_not_compress; FeatureFlagValues feature_flag_values; @@ -163,9 +162,11 @@ class LinkCommand : public Command { AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n" "defaults. Use this only when building runtime resource overlay packages.", &options_.no_resource_removal); - AddOptionalSwitch("--enable-sparse-encoding", - "This decreases APK size at the cost of resource retrieval performance.", - &options_.use_sparse_encoding); + AddOptionalSwitch( + "--enable-sparse-encoding", + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--enable-compact-entries", "This decreases APK size by using compact resource entries for simple data types.", &options_.table_flattener_options.use_compact_entries); diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 762441ee1872..f218307af578 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -406,9 +406,6 @@ int OptimizeCommand::Action(const std::vector<std::string>& args) { return 1; } - if (options_.enable_sparse_encoding) { - options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; - } if (options_.force_sparse_encoding) { options_.table_flattener_options.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index 012b0f230ca2..e3af584cbbd9 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -61,9 +61,6 @@ struct OptimizeOptions { // TODO(b/246489170): keep the old option and format until transform to the new one std::optional<std::string> shortened_paths_map_path; - // Whether sparse encoding should be used for O+ resources. - bool enable_sparse_encoding = false; - // Whether sparse encoding should be used for all resources. bool force_sparse_encoding = false; @@ -106,11 +103,9 @@ class OptimizeCommand : public Command { &kept_artifacts_); AddOptionalSwitch( "--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.\n" - "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " - "the APK is O+", - &options_.enable_sparse_encoding); + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index 1a82021bce71..b8ac7925d44e 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -201,7 +201,7 @@ class PackageFlattener { (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) { // Sparse encode if forced or sdk version is not set in context and config. } else { - // Otherwise, only sparse encode if the entries will be read on platforms S_V2+. + // Otherwise, only sparse encode if the entries will be read on platforms S_V2+ (32). sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2); } diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h index 0633bc81cb25..f1c4c3512ed3 100644 --- a/tools/aapt2/format/binary/TableFlattener.h +++ b/tools/aapt2/format/binary/TableFlattener.h @@ -37,8 +37,7 @@ constexpr const size_t kSparseEncodingThreshold = 60; enum class SparseEntriesMode { // Disables sparse encoding for entries. Disabled, - // Enables sparse encoding for all entries for APKs with O+ minSdk. For APKs with minSdk less - // than O only applies sparse encoding for resource configuration available on O+. + // Enables sparse encoding for all entries for APKs with minSdk >= 32 (S_V2). Enabled, // Enables sparse encoding for all entries regardless of minSdk. Forced, @@ -47,7 +46,7 @@ enum class SparseEntriesMode { struct TableFlattenerOptions { // When enabled, types for configurations with a sparse set of entries are encoded // as a sparse map of entry ID and offset to actual data. - SparseEntriesMode sparse_entries = SparseEntriesMode::Disabled; + SparseEntriesMode sparse_entries = SparseEntriesMode::Enabled; // When true, use compact entries for simple data bool use_compact_entries = false; diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index 0f1168514c4a..e3d589eb078b 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -337,13 +337,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Enabled; + options.sparse_entries = SparseEntriesMode::Disabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); @@ -421,13 +421,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Enabled; + options.sparse_entries = SparseEntriesMode::Disabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); diff --git a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt index 6da6fc6f12c3..d0807f2ecd34 100644 --- a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt +++ b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt @@ -877,7 +877,7 @@ resource_table { } tool_fingerprint { tool: "Android Asset Packaging Tool (aapt)" - version: "2.19-SOONG BUILD NUMBER PLACEHOLDER" + version: "2.20-SOONG BUILD NUMBER PLACEHOLDER" } } xml_files { diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md index 8368f9d16af8..664d8412a3be 100644 --- a/tools/aapt2/readme.md +++ b/tools/aapt2/readme.md @@ -1,5 +1,11 @@ # Android Asset Packaging Tool 2.0 (AAPT2) release notes +## Version 2.20 +- Too many features, bug fixes, and improvements to list since the last minor version update in + 2017. This README will be updated more frequently in the future. +- Sparse encoding is now always enabled by default if the minSdkVersion is >= 32 (S_V2). The + `--enable-sparse-encoding` flag still exists, but is a no-op. + ## Version 2.19 - Added navigation resource type. - Fixed issue with resource deduplication. (bug 64397629) diff --git a/tools/aapt2/util/Util.cpp b/tools/aapt2/util/Util.cpp index 3d83caf29bba..6a4dfa629394 100644 --- a/tools/aapt2/util/Util.cpp +++ b/tools/aapt2/util/Util.cpp @@ -227,7 +227,7 @@ std::string GetToolFingerprint() { static const char* const sMajorVersion = "2"; // Update minor version whenever a feature or flag is added. - static const char* const sMinorVersion = "19"; + static const char* const sMinorVersion = "20"; // The build id of aapt2 binary. static const std::string sBuildId = [] { diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt index ea660b013893..22d364ec3212 100644 --- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt +++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt @@ -263,7 +263,7 @@ object SystemFeaturesGenerator { .returns(Boolean::class.java) .addParameter(CONTEXT_CLASS, "context") .addParameter(String::class.java, "featureName") - .addStatement("return context.getPackageManager().hasSystemFeature(featureName, 0)") + .addStatement("return context.getPackageManager().hasSystemFeature(featureName)") .build() ) } diff --git a/tools/systemfeatures/tests/golden/RoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoFeatures.java.gen index ee97b26159de..730dacbbf995 100644 --- a/tools/systemfeatures/tests/golden/RoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RoFeatures.java.gen @@ -70,7 +70,7 @@ public final class RoFeatures { } private static boolean hasFeatureFallback(Context context, String featureName) { - return context.getPackageManager().hasSystemFeature(featureName, 0); + return context.getPackageManager().hasSystemFeature(featureName); } /** diff --git a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen index 40c7db7ff1df..fe268c70708e 100644 --- a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen @@ -25,7 +25,7 @@ public final class RoNoFeatures { } private static boolean hasFeatureFallback(Context context, String featureName) { - return context.getPackageManager().hasSystemFeature(featureName, 0); + return context.getPackageManager().hasSystemFeature(featureName); } /** diff --git a/tools/systemfeatures/tests/golden/RwFeatures.java.gen b/tools/systemfeatures/tests/golden/RwFeatures.java.gen index 7bf89614b92d..bcf978de3c1f 100644 --- a/tools/systemfeatures/tests/golden/RwFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RwFeatures.java.gen @@ -55,7 +55,7 @@ public final class RwFeatures { } private static boolean hasFeatureFallback(Context context, String featureName) { - return context.getPackageManager().hasSystemFeature(featureName, 0); + return context.getPackageManager().hasSystemFeature(featureName); } /** diff --git a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen index eb7ec63f1d7d..7bad5a2bae2a 100644 --- a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen @@ -14,7 +14,7 @@ import android.util.ArrayMap; */ public final class RwNoFeatures { private static boolean hasFeatureFallback(Context context, String featureName) { - return context.getPackageManager().hasSystemFeature(featureName, 0); + return context.getPackageManager().hasSystemFeature(featureName); } /** diff --git a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java index ed3f5c94ba79..491b55e7992c 100644 --- a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java +++ b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java @@ -76,28 +76,28 @@ public class SystemFeaturesGeneratorTest { // Also ensure we fall back to the PackageManager for feature APIs without an accompanying // versioned feature definition. - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true); assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false); assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse(); } @Test public void testReadonlyDisabledWithDefinedFeatures() { // Always fall back to the PackageManager for defined, explicit features queries. - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true); assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false); assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true); assertThat(RwFeatures.hasFeatureWifi(mContext)).isTrue(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN)).thenReturn(false); assertThat(RwFeatures.hasFeatureVulkan(mContext)).isFalse(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(false); assertThat(RwFeatures.hasFeatureAuto(mContext)).isFalse(); // For defined and undefined features, conditional queries should report null (unknown). @@ -139,9 +139,9 @@ public class SystemFeaturesGeneratorTest { assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 100)).isFalse(); // VERSION= - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(false); assertThat(RoFeatures.hasFeatureAuto(mContext)).isFalse(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(true); assertThat(RoFeatures.hasFeatureAuto(mContext)).isTrue(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, -1)).isNull(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); @@ -149,9 +149,9 @@ public class SystemFeaturesGeneratorTest { // For feature APIs without an associated feature definition, conditional queries should // report null, and explicit queries should report runtime-defined versions. - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC)).thenReturn(true); assertThat(RoFeatures.hasFeaturePc(mContext)).isTrue(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC)).thenReturn(false); assertThat(RoFeatures.hasFeaturePc(mContext)).isFalse(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_PC, -1)).isNull(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_PC, 0)).isNull(); |