Merge "Add method to create a deep copy of a ZenMode" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index d610f4c..c5a70df 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -34161,6 +34161,7 @@
field public static final int USAGE_CLASS_UNKNOWN = 0; // 0x0
field public static final int USAGE_COMMUNICATION_REQUEST = 65; // 0x41
field public static final int USAGE_HARDWARE_FEEDBACK = 50; // 0x32
+ field @FlaggedApi("android.os.vibrator.vibration_attribute_ime_usage_api") public static final int USAGE_IME_FEEDBACK = 82; // 0x52
field public static final int USAGE_MEDIA = 19; // 0x13
field public static final int USAGE_NOTIFICATION = 49; // 0x31
field public static final int USAGE_PHYSICAL_EMULATION = 34; // 0x22
diff --git a/core/java/android/app/Person.java b/core/java/android/app/Person.java
index 96f6f4e..c7432c5 100644
--- a/core/java/android/app/Person.java
+++ b/core/java/android/app/Person.java
@@ -189,10 +189,8 @@
*/
public void visitUris(@NonNull Consumer<Uri> visitor) {
visitor.accept(getIconUri());
- if (Flags.visitPersonUri()) {
- if (mUri != null && !mUri.isEmpty()) {
- visitor.accept(Uri.parse(mUri));
- }
+ if (mUri != null && !mUri.isEmpty()) {
+ visitor.accept(Uri.parse(mUri));
}
}
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 19de793..a1ae9da 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -84,6 +84,13 @@
}
flag {
+ namespace: "virtual_devices"
+ name: "enforce_remote_device_opt_out_on_all_virtual_displays"
+ description: "Respect canDisplayOnRemoteDevices on all virtual displays"
+ bug: "338973239"
+}
+
+flag {
namespace: "virtual_devices"
name: "virtual_display_multi_window_mode_support"
description: "Add support for WINDOWING_MODE_MULTI_WINDOW to virtual displays by default"
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 6952a09..481e6b5 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -617,7 +617,7 @@
*/
public static final int FLAG_ENABLE_VR_MODE = 0x8000;
/**
- * Bit in {@link #flags} indicating if the activity can be displayed on a remote device.
+ * Bit in {@link #flags} indicating if the activity can be displayed on a virtual display.
* Corresponds to {@link android.R.attr#canDisplayOnRemoteDevices}
* @hide
*/
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 88fbbdd..6882d5c 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -359,3 +359,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "show_different_creation_error_for_unsupported_devices"
+ namespace: "profile_experiences"
+ description: "On private space create error due to child account added/fully managed user show message with link to the Help Center to find out more."
+ bug: "340130375"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index fe14d457..00ce949 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -1171,12 +1171,14 @@
}
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_HOVER_ENTER:
// Consume and ignore all touches while stylus is down to prevent
// accidental touches from going to the app while writing.
mPrivOps.setHandwritingSurfaceNotTouchable(false);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_HOVER_EXIT:
// Go back to only consuming stylus events so that the user
// can continue to interact with the app using touch
// when the stylus is not down.
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 136c45d..47096db 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -434,7 +434,6 @@
@RavenwoodThrow
private static native void nativeWriteStrongBinder(long nativePtr, IBinder val);
@FastNative
- @RavenwoodThrow
private static native void nativeWriteFileDescriptor(long nativePtr, FileDescriptor val);
private static native byte[] nativeCreateByteArray(long nativePtr);
@@ -456,7 +455,6 @@
@RavenwoodThrow
private static native IBinder nativeReadStrongBinder(long nativePtr);
@FastNative
- @RavenwoodThrow
private static native FileDescriptor nativeReadFileDescriptor(long nativePtr);
private static native long nativeCreate();
diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java
index 71957ee..464df23 100644
--- a/core/java/android/os/ParcelFileDescriptor.java
+++ b/core/java/android/os/ParcelFileDescriptor.java
@@ -381,6 +381,8 @@
}
private static void closeInternal$ravenwood(FileDescriptor fd) {
+ // Desktop JVM doesn't have FileDescriptor.close(), so we'll need to go to the ravenwood
+ // side to close it.
native_close$ravenwood(fd);
}
diff --git a/core/java/android/os/VibrationAttributes.java b/core/java/android/os/VibrationAttributes.java
index 9df5b85..da863e5 100644
--- a/core/java/android/os/VibrationAttributes.java
+++ b/core/java/android/os/VibrationAttributes.java
@@ -16,6 +16,9 @@
package android.os;
+import static android.os.vibrator.Flags.FLAG_VIBRATION_ATTRIBUTE_IME_USAGE_API;
+
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -55,6 +58,7 @@
USAGE_PHYSICAL_EMULATION,
USAGE_RINGTONE,
USAGE_TOUCH,
+ USAGE_IME_FEEDBACK,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Usage {}
@@ -136,6 +140,12 @@
*/
public static final int USAGE_ACCESSIBILITY = 0x40 | USAGE_CLASS_FEEDBACK;
/**
+ * Usage value to use for input method editor (IME) haptic feedback.
+ */
+ @FlaggedApi(FLAG_VIBRATION_ATTRIBUTE_IME_USAGE_API)
+ public static final int USAGE_IME_FEEDBACK = 0x50 | USAGE_CLASS_FEEDBACK;
+
+ /**
* Usage value to use for media vibrations, such as music, movie, soundtrack, animations, games,
* or any interactive media that isn't for touch feedback specifically.
*/
@@ -174,7 +184,6 @@
FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF,
FLAG_INVALIDATE_SETTINGS_CACHE,
FLAG_PIPELINED_EFFECT,
- FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE
})
@Retention(RetentionPolicy.SOURCE)
public @interface Flag{}
@@ -228,31 +237,12 @@
public static final int FLAG_PIPELINED_EFFECT = 1 << 3;
/**
- * Flag requesting that this vibration effect to be played without applying the user
- * intensity setting to scale the vibration.
- *
- * <p>The user setting is still applied to enable/disable the vibration, but the vibration
- * effect strength will not be scaled based on the enabled setting value.
- *
- * <p>This is intended to be used on scenarios where the system needs to enforce a specific
- * strength for the vibration effect, regardless of the user preference. Only privileged apps
- * can ignore user settings, and this flag will be ignored otherwise.
- *
- * <p>If you need to bypass the user setting when it's disabling vibrations then this also
- * needs the flag {@link #FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF} to be set.
- *
- * @hide
- */
- public static final int FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE = 1 << 4;
-
- /**
* All flags supported by vibrator service, update it when adding new flag.
* @hide
*/
public static final int FLAG_ALL_SUPPORTED =
FLAG_BYPASS_INTERRUPTION_POLICY | FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF
- | FLAG_INVALIDATE_SETTINGS_CACHE | FLAG_PIPELINED_EFFECT
- | FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE;
+ | FLAG_INVALIDATE_SETTINGS_CACHE | FLAG_PIPELINED_EFFECT;
/** Creates a new {@link VibrationAttributes} instance with given usage. */
public static @NonNull VibrationAttributes createForUsage(@Usage int usage) {
@@ -349,6 +339,7 @@
case USAGE_RINGTONE:
return AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
case USAGE_TOUCH:
+ case USAGE_IME_FEEDBACK:
return AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;
case USAGE_ALARM:
return AudioAttributes.USAGE_ALARM;
@@ -447,6 +438,8 @@
return "PHYSICAL_EMULATION";
case USAGE_HARDWARE_FEEDBACK:
return "HARDWARE_FEEDBACK";
+ case USAGE_IME_FEEDBACK:
+ return "IME";
default:
return "unknown usage " + usage;
}
diff --git a/core/java/android/os/vibrator/VibrationConfig.java b/core/java/android/os/vibrator/VibrationConfig.java
index f6e73b3..a4164e9 100644
--- a/core/java/android/os/vibrator/VibrationConfig.java
+++ b/core/java/android/os/vibrator/VibrationConfig.java
@@ -20,6 +20,7 @@
import static android.os.VibrationAttributes.USAGE_ALARM;
import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK;
+import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK;
import static android.os.VibrationAttributes.USAGE_MEDIA;
import static android.os.VibrationAttributes.USAGE_NOTIFICATION;
import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION;
@@ -67,6 +68,8 @@
private final int mDefaultNotificationVibrationIntensity;
@VibrationIntensity
private final int mDefaultRingVibrationIntensity;
+ @VibrationIntensity
+ private final int mDefaultKeyboardVibrationIntensity;
private final boolean mKeyboardVibrationSettingsSupported;
@@ -98,6 +101,8 @@
com.android.internal.R.integer.config_defaultNotificationVibrationIntensity);
mDefaultRingVibrationIntensity = loadDefaultIntensity(resources,
com.android.internal.R.integer.config_defaultRingVibrationIntensity);
+ mDefaultKeyboardVibrationIntensity = loadDefaultIntensity(resources,
+ com.android.internal.R.integer.config_defaultKeyboardVibrationIntensity);
}
@VibrationIntensity
@@ -213,6 +218,9 @@
case USAGE_PHYSICAL_EMULATION:
case USAGE_ACCESSIBILITY:
return mDefaultHapticFeedbackIntensity;
+ case USAGE_IME_FEEDBACK:
+ return isKeyboardVibrationSettingsSupported()
+ ? mDefaultKeyboardVibrationIntensity : mDefaultHapticFeedbackIntensity;
case USAGE_MEDIA:
case USAGE_UNKNOWN:
// fall through
@@ -236,6 +244,7 @@
+ ", mDefaultMediaIntensity=" + mDefaultMediaVibrationIntensity
+ ", mDefaultNotificationIntensity=" + mDefaultNotificationVibrationIntensity
+ ", mDefaultRingIntensity=" + mDefaultRingVibrationIntensity
+ + ", mDefaultKeyboardIntensity=" + mDefaultKeyboardVibrationIntensity
+ ", mKeyboardVibrationSettingsSupported=" + mKeyboardVibrationSettingsSupported
+ "}";
}
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index 62b3682..67c3464 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -74,3 +74,14 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "haptics"
+ name: "vibration_attribute_ime_usage_api"
+ is_exported: true
+ description: "A public API for IME usage vibration attribute"
+ bug: "332661766"
+ metadata {
+ purpose: PURPOSE_FEATURE
+ }
+}
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
index a78a417..6b1aef7 100644
--- a/core/java/android/text/DynamicLayout.java
+++ b/core/java/android/text/DynamicLayout.java
@@ -1320,7 +1320,11 @@
// It's possible that a Span is removed when the text covering it is
// deleted, in this case, the original start and end of the span might be
// OOB. So it'll reflow the entire string instead.
- reflow(s, 0, 0, s.length());
+ if (Flags.insertModeCrashUpdateLayoutSpan()) {
+ transformAndReflow(s, 0, s.length());
+ } else {
+ reflow(s, 0, 0, s.length());
+ }
} else {
reflow(s, start, end - start, end - start);
}
@@ -1343,7 +1347,11 @@
// When text is changed, it'll also trigger onSpanChanged. In this case we
// can't determine the updated range in the transformed text. So it'll
// reflow the entire range instead.
- reflow(s, 0, 0, s.length());
+ if (Flags.insertModeCrashUpdateLayoutSpan()) {
+ transformAndReflow(s, 0, s.length());
+ } else {
+ reflow(s, 0, 0, s.length());
+ }
} else {
reflow(s, start, end - start, end - start);
reflow(s, nstart, nend - nstart, nend - nstart);
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 155a3e4..88a1b9c 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -125,6 +125,16 @@
}
flag {
+ name: "insert_mode_crash_update_layout_span"
+ namespace: "text"
+ description: "Fix insert mode crash when the text has UpdateLayout span attached."
+ bug: "355137282"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "icu_bidi_migration"
namespace: "text"
description: "A flag for replacing AndroidBidi with android.icu.text.Bidi."
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 725d496..e5a9b6a 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -65,14 +65,6 @@
}
flag {
- name: "defer_display_updates"
- namespace: "windowing_frontend"
- description: "Feature flag for deferring DisplayManager updates to WindowManager if Shell transition is running"
- bug: "259220649"
- is_fixed_read_only: true
-}
-
-flag {
name: "close_to_square_config_includes_status_bar"
namespace: "windowing_frontend"
description: "On close to square display, when necessary, configuration includes status bar"
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index ab456a8..6258f5c 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -544,6 +544,14 @@
@Override
protected void onCreate(Bundle savedInstanceState) {
+ if (Settings.Secure.getIntForUser(getContentResolver(),
+ Settings.Secure.SECURE_FRP_MODE, 0,
+ getUserId()) == 1) {
+ Log.e(TAG, "Sharing disabled due to active FRP lock.");
+ super.onCreate(savedInstanceState);
+ finish();
+ return;
+ }
final long intentReceivedTime = System.currentTimeMillis();
mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 2e3dbda..0be33c2 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -3296,8 +3296,8 @@
usually TVs.
<p>Requires permission {@code android.permission.DISABLE_SYSTEM_SOUND_EFFECTS}. -->
<attr name="playHomeTransitionSound" format="boolean"/>
- <!-- Indicates whether the activity can be displayed on a remote device which may or
- may not be running Android. -->
+ <!-- Indicates whether the activity can be displayed on a display that may belong to a
+ remote device which may or may not be running Android. -->
<attr name="canDisplayOnRemoteDevices" format="boolean"/>
<attr name="allowUntrustedActivityEmbedding" />
<attr name="knownActivityEmbeddingCerts" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 2afc303..8ed444d 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1399,6 +1399,10 @@
Settings.System.RING_VIBRATION_INTENSITY more details on the constant values and
meanings. -->
<integer name="config_defaultRingVibrationIntensity">2</integer>
+ <!-- The default intensity level for keyboard vibrations. Note that this will only be applied
+ on devices where config_keyboardVibrationSettingsSupported is true, otherwise the
+ keyboard vibration will follow config_defaultHapticFeedbackIntensity -->
+ <integer name="config_defaultKeyboardVibrationIntensity">2</integer>
<!-- Whether to use the strict phone number matcher by default. -->
<bool name="config_use_strict_phone_number_comparation">false</bool>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index bc8c778..6b58396 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4223,6 +4223,7 @@
<java-symbol type="integer" name="config_defaultMediaVibrationIntensity" />
<java-symbol type="integer" name="config_defaultNotificationVibrationIntensity" />
<java-symbol type="integer" name="config_defaultRingVibrationIntensity" />
+ <java-symbol type="integer" name="config_defaultKeyboardVibrationIntensity" />
<java-symbol type="bool" name="config_maskMainBuiltInDisplayCutout" />
diff --git a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
index a102b3e..eb463fd 100644
--- a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
+++ b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
@@ -30,9 +30,9 @@
import android.view.Choreographer;
import android.view.animation.LinearInterpolator;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.rule.ActivityTestRule;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
diff --git a/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java b/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java
index e025fae..b91263e 100644
--- a/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java
+++ b/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java
@@ -35,7 +35,7 @@
import android.platform.test.annotations.Presubmit;
import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Rule;
diff --git a/core/tests/coretests/src/android/debug/AdbNotificationsTest.java b/core/tests/coretests/src/android/debug/AdbNotificationsTest.java
index 3496e2c..10eeb35 100644
--- a/core/tests/coretests/src/android/debug/AdbNotificationsTest.java
+++ b/core/tests/coretests/src/android/debug/AdbNotificationsTest.java
@@ -25,8 +25,8 @@
import android.text.TextUtils;
import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
diff --git a/core/tests/coretests/src/android/graphics/FontListParserTest.java b/core/tests/coretests/src/android/graphics/FontListParserTest.java
index 5f96c17..52f53dd 100644
--- a/core/tests/coretests/src/android/graphics/FontListParserTest.java
+++ b/core/tests/coretests/src/android/graphics/FontListParserTest.java
@@ -16,16 +16,16 @@
package android.graphics;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT;
import static android.graphics.fonts.FontStyle.FONT_SLANT_ITALIC;
import static android.graphics.fonts.FontStyle.FONT_SLANT_UPRIGHT;
import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL;
import static android.text.FontConfig.FontFamily.VARIANT_COMPACT;
import static android.text.FontConfig.FontFamily.VARIANT_DEFAULT;
import static android.text.FontConfig.FontFamily.VARIANT_ELEGANT;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT;
import static com.google.common.truth.Truth.assertThat;
@@ -38,8 +38,8 @@
import android.text.FontConfig;
import android.util.Xml;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/graphics/RectTest.java b/core/tests/coretests/src/android/graphics/RectTest.java
index 2918f44..d0cb5d5 100644
--- a/core/tests/coretests/src/android/graphics/RectTest.java
+++ b/core/tests/coretests/src/android/graphics/RectTest.java
@@ -24,8 +24,8 @@
import android.platform.test.annotations.Presubmit;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java b/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java
index 6ae7eb7..a94f412 100644
--- a/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java
+++ b/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java
@@ -23,8 +23,8 @@
import android.graphics.fonts.Font;
import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
index 0d687b2..10aed8d 100644
--- a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
+++ b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
@@ -39,8 +39,8 @@
import android.util.ArrayMap;
import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
diff --git a/core/tests/coretests/src/android/graphics/TypefaceTest.java b/core/tests/coretests/src/android/graphics/TypefaceTest.java
index 6bf8f56..80efa51 100644
--- a/core/tests/coretests/src/android/graphics/TypefaceTest.java
+++ b/core/tests/coretests/src/android/graphics/TypefaceTest.java
@@ -30,10 +30,10 @@
import android.util.ArrayMap;
import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.frameworks.coretests.R;
diff --git a/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java b/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java
index d0a6ff9..4991cd0 100644
--- a/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java
+++ b/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java
@@ -25,8 +25,8 @@
import android.graphics.PorterDuffXfermode;
import android.graphics.Xfermode;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java b/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java
index 5aeab42..b4f1dee 100644
--- a/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java
+++ b/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java
@@ -21,8 +21,8 @@
import android.platform.test.annotations.Presubmit;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java b/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java
index 0deb77e..55a347e 100644
--- a/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java
+++ b/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java
@@ -27,8 +27,8 @@
import android.widget.ImageView;
import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
diff --git a/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java b/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java
index c25aa51..746c8ca 100644
--- a/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java
+++ b/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java
@@ -42,9 +42,9 @@
import android.print.test.services.StubbablePrinterDiscoverySession;
import android.printservice.recommendation.IRecommendationsChangeListener;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
import androidx.test.uiautomator.UiDevice;
import org.junit.Before;
diff --git a/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java b/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java
index e20258a..a60746f 100644
--- a/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java
+++ b/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java
@@ -23,8 +23,8 @@
import android.platform.test.annotations.Presubmit;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
diff --git a/core/tests/coretests/src/android/provider/DeviceConfigTest.java b/core/tests/coretests/src/android/provider/DeviceConfigTest.java
index 9300d1e..681396e 100644
--- a/core/tests/coretests/src/android/provider/DeviceConfigTest.java
+++ b/core/tests/coretests/src/android/provider/DeviceConfigTest.java
@@ -29,9 +29,9 @@
import android.platform.test.annotations.Presubmit;
import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.FlakyTest;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Assert;
diff --git a/core/tests/coretests/src/android/provider/FontsContractE2ETest.java b/core/tests/coretests/src/android/provider/FontsContractE2ETest.java
index 7e02be8..4010171 100644
--- a/core/tests/coretests/src/android/provider/FontsContractE2ETest.java
+++ b/core/tests/coretests/src/android/provider/FontsContractE2ETest.java
@@ -33,8 +33,8 @@
import android.provider.FontsContract.FontFamilyResult;
import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
diff --git a/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java b/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java
index 4d446901..6eaf2e4 100644
--- a/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java
+++ b/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java
@@ -45,9 +45,9 @@
import android.service.controls.actions.ControlActionWrapper;
import android.service.controls.templates.ThumbnailTemplate;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
import com.android.internal.R;
diff --git a/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java b/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java
index d8088b7..44bdc53 100644
--- a/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java
+++ b/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java
@@ -23,8 +23,8 @@
import android.os.Parcel;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java b/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
index 91a3ba7..73b6f648 100644
--- a/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
+++ b/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
@@ -25,8 +25,8 @@
import android.graphics.drawable.Icon;
import android.os.Parcel;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.frameworks.coretests.R;
diff --git a/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java b/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java
index 6792d0b..f4206c8 100644
--- a/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java
+++ b/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java
@@ -26,8 +26,8 @@
import android.service.carrier.CarrierIdentifier;
import android.telephony.UiccAccessRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java b/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java
index a121941..44456e9 100644
--- a/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java
+++ b/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java
@@ -27,8 +27,8 @@
import android.os.Parcel;
import android.util.ArraySet;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java b/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java
index 76c9f88..5042408 100644
--- a/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java
+++ b/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java
@@ -37,8 +37,8 @@
import android.os.UserHandle;
import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
diff --git a/core/tests/coretests/src/android/service/quicksettings/TileTest.java b/core/tests/coretests/src/android/service/quicksettings/TileTest.java
index ca6c3b4..43f9122 100644
--- a/core/tests/coretests/src/android/service/quicksettings/TileTest.java
+++ b/core/tests/coretests/src/android/service/quicksettings/TileTest.java
@@ -18,8 +18,8 @@
import static com.google.common.truth.Truth.assertThat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java
index 64edda5..85659d6 100644
--- a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java
+++ b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java
@@ -23,9 +23,9 @@
import android.os.RemoteException;
import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.rule.ServiceTestRule;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
diff --git a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java
index e0eb197..03096de 100644
--- a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java
+++ b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java
@@ -26,8 +26,8 @@
import android.os.Parcel;
import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
diff --git a/core/tests/coretests/src/android/telephony/PinResultTest.java b/core/tests/coretests/src/android/telephony/PinResultTest.java
index c260807..f5432ee 100644
--- a/core/tests/coretests/src/android/telephony/PinResultTest.java
+++ b/core/tests/coretests/src/android/telephony/PinResultTest.java
@@ -18,7 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java b/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java
index 5ff659b..8a41678 100644
--- a/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java
+++ b/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java
@@ -21,11 +21,18 @@
import static com.google.common.truth.Truth.assertThat;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.text.method.OffsetMapping;
+import android.text.style.UpdateLayout;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.text.flags.Flags;
+
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -36,6 +43,9 @@
private static final int WIDTH = 10000;
private static final TextPaint sTextPaint = new TextPaint();
+ @Rule
+ public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Test
public void textWithOffsetMapping() {
final String text = "abcde";
@@ -120,6 +130,84 @@
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_CRASH_UPDATE_LAYOUT_SPAN)
+ public void textWithOffsetMapping_deletion_withUpdateLayoutSpan() {
+ final String text = "abcdef";
+ final SpannableStringBuilder spannable = new SpannableStringBuilder(text);
+ // UpdateLayout span covers the letter 'd'.
+ spannable.setSpan(new UpdateLayout() {}, 3, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ final CharSequence transformedText =
+ new TestOffsetMapping(spannable, 3, "\n\n");
+
+ final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH)
+ .setAlignment(ALIGN_NORMAL)
+ .setIncludePad(false)
+ .setDisplayText(transformedText)
+ .build();
+
+ // delete character 'c', original text becomes "abdef"
+ spannable.delete(2, 3);
+ assertThat(transformedText.toString()).isEqualTo("ab\n\ndef");
+ assertLineRange(layout, /* lineBreaks */ 0, 3, 4, 7);
+
+ // delete character 'd', original text becomes "abef"
+ spannable.delete(2, 3);
+ assertThat(transformedText.toString()).isEqualTo("ab\n\nef");
+ assertLineRange(layout, /* lineBreaks */ 0, 3, 4, 6);
+
+ // delete "be", original text becomes "af"
+ spannable.delete(1, 3);
+ assertThat(transformedText.toString()).isEqualTo("a\n\nf");
+ assertLineRange(layout, /* lineBreaks */ 0, 2, 3, 4);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_CRASH_UPDATE_LAYOUT_SPAN)
+ public void textWithOffsetMapping_insert_withUpdateLayoutSpan() {
+ final String text = "abcdef";
+ final SpannableStringBuilder spannable = new SpannableStringBuilder(text);
+ final CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n");
+
+ // UpdateLayout span covers the letter 'de'.
+ spannable.setSpan(new UpdateLayout() {}, 3, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH)
+ .setAlignment(ALIGN_NORMAL)
+ .setIncludePad(false)
+ .setDisplayText(transformedText)
+ .build();
+
+ spannable.insert(3, "x");
+ assertThat(transformedText.toString()).isEqualTo("abcx\n\ndef");
+ assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 9);
+
+ spannable.insert(5, "x");
+ assertThat(transformedText.toString()).isEqualTo("abcx\n\ndxef");
+ assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 10);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_CRASH_UPDATE_LAYOUT_SPAN)
+ public void textWithOffsetMapping_replace_withUpdateLayoutSpan() {
+ final String text = "abcdef";
+ final SpannableStringBuilder spannable = new SpannableStringBuilder(text);
+ final CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n");
+ // UpdateLayout span covers the letter 'de'.
+ spannable.setSpan(new UpdateLayout() {}, 3, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH)
+ .setAlignment(ALIGN_NORMAL)
+ .setIncludePad(false)
+ .setDisplayText(transformedText)
+ .build();
+
+ spannable.replace(2, 4, "xx");
+ assertThat(transformedText.toString()).isEqualTo("abxx\n\nef");
+ assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 8);
+ }
+
+ @Test
public void textWithOffsetMapping_blockBeforeTextChanged_deletion() {
final String text = "abcdef";
final SpannableStringBuilder spannable = new TestNoBeforeTextChangeSpannableString(text);
diff --git a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
index df9a89e..bbeb18d 100644
--- a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
+++ b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
@@ -37,7 +37,7 @@
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.truth.Truth;
import com.google.protobuf.InvalidProtocolBufferException;
diff --git a/core/tests/coretests/src/android/transition/AutoTransitionTest.java b/core/tests/coretests/src/android/transition/AutoTransitionTest.java
index deae967..5d58fead 100644
--- a/core/tests/coretests/src/android/transition/AutoTransitionTest.java
+++ b/core/tests/coretests/src/android/transition/AutoTransitionTest.java
@@ -20,8 +20,8 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java b/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java
index 3a27225..178e93a 100644
--- a/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java
+++ b/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java
@@ -22,7 +22,7 @@
import android.os.Parcel;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java b/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java
index 725dcf3..3d1b565 100644
--- a/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java
+++ b/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java
@@ -29,8 +29,8 @@
import android.os.UserHandle;
import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
import com.android.frameworks.coretests.aidl.ITestServiceConnectorService;
import com.android.internal.infra.ServiceConnectorTest.CapturingServiceLifecycleCallbacks.ServiceLifeCycleEvent;
diff --git a/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java b/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java
index 7054cc0..b86cb4a 100644
--- a/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java
+++ b/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java
@@ -20,8 +20,8 @@
import android.metrics.LogMaker;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.logging.testing.FakeMetricsLogger;
diff --git a/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java b/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java
index 7840f71..fc28627 100644
--- a/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java
+++ b/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java
@@ -18,8 +18,8 @@
import static com.google.common.truth.Truth.assertThat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.internal.logging.testing.UiEventLoggerFake;
diff --git a/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java b/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java
index d1ef61b..d1c0668 100644
--- a/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java
+++ b/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java
@@ -19,7 +19,7 @@
import android.platform.test.ravenwood.RavenwoodRule;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 269a586..606ebb4 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -554,15 +554,10 @@
enable_windowing_edge_drag_resize is disabled. -->
<dimen name="freeform_resize_corner">44dp</dimen>
- <!-- The width of the area at the sides of the screen where a freeform task will transition to
- split select if dragged until the touch input is within the range. -->
- <dimen name="desktop_mode_transition_area_width">32dp</dimen>
+ <!-- The thickness in dp for all desktop drag transition regions. -->
+ <dimen name="desktop_mode_transition_region_thickness">44dp</dimen>
- <!-- The width of the area where a desktop task will transition to fullscreen. -->
- <dimen name="desktop_mode_fullscreen_from_desktop_width">80dp</dimen>
-
- <!-- The height of the area where a desktop task will transition to fullscreen. -->
- <dimen name="desktop_mode_fullscreen_from_desktop_height">40dp</dimen>
+ <item type="dimen" format="float" name="desktop_mode_fullscreen_region_scale">0.4</item>
<!-- The height on the screen where drag to the left or right edge will result in a
desktop task snapping to split size. The empty space between this and the top is to allow
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
index 2b01eac..1304969 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
@@ -43,7 +43,8 @@
APP_HEADER_WITH_TASK_DENSITY(Flags::enableAppHeaderWithTaskDensity, true),
TASK_STACK_OBSERVER_IN_SHELL(Flags::enableTaskStackObserverInShell, true),
SIZE_CONSTRAINTS(Flags::enableDesktopWindowingSizeConstraints, true),
- DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, true);
+ DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, true),
+ ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true);
/**
* Determines state of flag based on the actual flag and desktop mode developer option overrides.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 35d3876..0262507 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -16,7 +16,7 @@
package com.android.wm.shell.dagger;
-import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT;
import android.annotation.Nullable;
import android.app.KeyguardManager;
@@ -569,7 +569,7 @@
ShellTaskOrganizer shellTaskOrganizer) {
int maxTaskLimit = DesktopModeStatus.getMaxTaskLimit(context);
if (!DesktopModeStatus.canEnterDesktopMode(context)
- || !DESKTOP_WINDOWING_MODE.isEnabled(context)
+ || !ENABLE_DESKTOP_WINDOWING_TASK_LIMIT.isEnabled(context)
|| maxTaskLimit <= 0) {
return Optional.empty();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 4299841..4bec788 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -270,13 +270,37 @@
logW("Unminimize Task: display=%d, task=%d, no task data", displayId, taskId)
}
- /** Removes task from the ordered list. */
+ private fun getDisplayIdForTask(taskId: Int): Int? {
+ desktopTaskDataByDisplayId.forEach { displayId, data ->
+ if (taskId in data.freeformTasksInZOrder) {
+ return displayId
+ }
+ }
+ logW("No display id found for task: taskId=%d", taskId)
+ return null
+ }
+
+ /**
+ * Removes [taskId] from the respective display. If [INVALID_DISPLAY], the original display id
+ * will be looked up from the task id.
+ */
fun removeFreeformTask(displayId: Int, taskId: Int) {
logD("Removes freeform task: taskId=%d", taskId)
+ if (displayId == INVALID_DISPLAY) {
+ // Removes the original display id of the task.
+ getDisplayIdForTask(taskId)?.let { removeTaskFromDisplay(it, taskId) }
+ } else {
+ removeTaskFromDisplay(displayId, taskId)
+ }
+ }
+
+ /** Removes given task from a valid [displayId]. */
+ private fun removeTaskFromDisplay(displayId: Int, taskId: Int) {
+ logD("Removes freeform task: taskId=%d, displayId=%d", taskId, displayId)
desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.remove(taskId)
boundsBeforeMaximizeByTaskId.remove(taskId)
- logD("Remaining freeform tasks: %d",
- desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.toDumpString() ?: "")
+ logD("Remaining freeform tasks: %s",
+ desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.toDumpString())
}
/**
@@ -358,3 +382,4 @@
private fun <T> Iterable<T>.toDumpString(): String =
joinToString(separator = ", ", prefix = "[", postfix = "]")
+
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index ed0d2b8..6011db7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -105,7 +105,7 @@
// If we are in freeform, we don't want a visible indicator in the "freeform" drag zone.
IndicatorType result = IndicatorType.NO_INDICATOR;
final int transitionAreaWidth = mContext.getResources().getDimensionPixelSize(
- com.android.wm.shell.R.dimen.desktop_mode_transition_area_width);
+ com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness);
// Because drags in freeform use task position for indicator calculation, we need to
// account for the possibility of the task going off the top of the screen by captionHeight
final int captionHeight = mContext.getResources().getDimensionPixelSize(
@@ -140,18 +140,19 @@
final Region region = new Region();
int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM
? mContext.getResources().getDimensionPixelSize(
- com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height)
+ com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness)
: 2 * layout.stableInsets().top;
- // A thin, short Rect at the top of the screen.
+ // A Rect at the top of the screen that takes up the center 40%.
if (windowingMode == WINDOWING_MODE_FREEFORM) {
- int fromFreeformWidth = mContext.getResources().getDimensionPixelSize(
- com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_width);
- region.union(new Rect((layout.width() / 2) - (fromFreeformWidth / 2),
+ final float toFullscreenScale = mContext.getResources().getFloat(
+ R.dimen.desktop_mode_fullscreen_region_scale);
+ final float toFullscreenWidth = (layout.width() * toFullscreenScale);
+ region.union(new Rect((int) ((layout.width() / 2f) - (toFullscreenWidth / 2f)),
-captionHeight,
- (layout.width() / 2) + (fromFreeformWidth / 2),
+ (int) ((layout.width() / 2f) + (toFullscreenWidth / 2f)),
transitionHeight));
}
- // A screen-wide, shorter Rect if the task is in fullscreen or split.
+ // A screen-wide Rect if the task is in fullscreen or split.
if (windowingMode == WINDOWING_MODE_FULLSCREEN
|| windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
region.union(new Rect(0,
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 a011ff5..87d1800 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
@@ -33,7 +33,8 @@
* Limits the number of tasks shown in Desktop Mode.
*
* This class should only be used if
- * [com.android.window.flags.Flags.enableDesktopWindowingTaskLimit()] is true.
+ * [com.android.wm.shell.shared.desktopmode.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT]
+ * is enabled and [maxTasksLimit] is strictly greater than 0.
*/
class DesktopTasksLimiter (
transitions: Transitions,
@@ -86,10 +87,10 @@
}
/**
- * Returns whether the given Task is being reordered to the back in the given transition, or
- * is already invisible.
+ * Returns whether the Task [taskDetails] is being reordered to the back in the transition
+ * [info], or is already invisible.
*
- * <p> This check can be used to double-check that a task was indeed minimized before
+ * This check can be used to double-check that a task was indeed minimized before
* marking it as such.
*/
private fun isTaskReorderedToBackOrInvisible(
@@ -149,8 +150,8 @@
}
/**
- * Mark a task as minimized, this should only be done after the corresponding transition has
- * finished so we don't minimize the task if the transition fails.
+ * Mark [taskId], which must be on [displayId], as minimized, this should only be done after the
+ * corresponding transition has finished so we don't minimize the task if the transition fails.
*/
private fun markTaskMinimized(displayId: Int, taskId: Int) {
ProtoLog.v(
@@ -161,11 +162,9 @@
/**
* Add a minimize-transition to [wct] if adding [newFrontTaskInfo] brings us over the task
- * limit.
+ * limit, returning the task to minimize.
*
- * @param transition the transition that the minimize-transition will be appended to, or null if
- * the transition will be started later.
- * @return the ID of the minimized task, or null if no task is being minimized.
+ * The task must be on [displayId].
*/
fun addAndGetMinimizeTaskChangesIfNeeded(
displayId: Int,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 7451d22..284620e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -272,6 +272,7 @@
final boolean changed = onDisplayRotationChanged(mContext, outBounds, currentBounds,
mTmpInsetBounds, displayId, fromRotation, toRotation, t);
if (changed) {
+ mMenuController.hideMenu();
// If the pip was in the offset zone earlier, adjust the new bounds to the bottom of the
// movement bounds
mTouchHandler.adjustBoundsForRotation(outBounds, mPipBoundsState.getBounds(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 0a5672d..9dc3401 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -468,8 +468,42 @@
}
@Test
+ fun removeFreeformTask_invalidDisplay_removesTaskFromFreeformTasks() {
+ repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1)
+
+ repo.removeFreeformTask(INVALID_DISPLAY, taskId = 1)
+
+ val invalidDisplayTasks = repo.getFreeformTasksInZOrder(INVALID_DISPLAY)
+ assertThat(invalidDisplayTasks).isEmpty()
+ val validDisplayTasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
+ assertThat(validDisplayTasks).isEmpty()
+ }
+
+ @Test
+ fun removeFreeformTask_validDisplay_removesTaskFromFreeformTasks() {
+ repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1)
+
+ repo.removeFreeformTask(DEFAULT_DISPLAY, taskId = 1)
+
+ val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
+ assertThat(tasks).isEmpty()
+ }
+
+ @Test
+ fun removeFreeformTask_validDisplay_differentDisplay_doesNotRemovesTask() {
+ repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1)
+
+ repo.removeFreeformTask(SECOND_DISPLAY, taskId = 1)
+
+ val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
+ assertThat(tasks).containsExactly(1)
+ }
+
+ @Test
fun removeFreeformTask_removesTaskBoundsBeforeMaximize() {
val taskId = 1
+ repo.addActiveTask(THIRD_DISPLAY, taskId)
+ repo.addOrMoveFreeformTaskToTop(THIRD_DISPLAY, taskId)
repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200))
repo.removeFreeformTask(THIRD_DISPLAY, taskId)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
index bd39aa6..2dea43b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
@@ -61,20 +61,23 @@
@Test
fun testFullscreenRegionCalculation() {
- val transitionHeight = context.resources.getDimensionPixelSize(
- R.dimen.desktop_mode_fullscreen_from_desktop_height)
- val fromFreeformWidth = mContext.resources.getDimensionPixelSize(
- R.dimen.desktop_mode_fullscreen_from_desktop_width
- )
var testRegion = visualIndicator.calculateFullscreenRegion(displayLayout,
WINDOWING_MODE_FULLSCREEN, CAPTION_HEIGHT)
assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, 2 * STABLE_INSETS.top))
testRegion = visualIndicator.calculateFullscreenRegion(displayLayout,
WINDOWING_MODE_FREEFORM, CAPTION_HEIGHT)
+
+ val transitionHeight = context.resources.getDimensionPixelSize(
+ R.dimen.desktop_mode_transition_region_thickness)
+ val toFullscreenScale = mContext.resources.getFloat(
+ R.dimen.desktop_mode_fullscreen_region_scale
+ )
+ val toFullscreenWidth = displayLayout.width() * toFullscreenScale
+
assertThat(testRegion.bounds).isEqualTo(Rect(
- DISPLAY_BOUNDS.width() / 2 - fromFreeformWidth / 2,
+ (DISPLAY_BOUNDS.width() / 2f - toFullscreenWidth / 2f).toInt(),
-50,
- DISPLAY_BOUNDS.width() / 2 + fromFreeformWidth / 2,
+ (DISPLAY_BOUNDS.width() / 2f + toFullscreenWidth / 2f).toInt(),
transitionHeight))
testRegion = visualIndicator.calculateFullscreenRegion(displayLayout,
WINDOWING_MODE_MULTI_WINDOW, CAPTION_HEIGHT)
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index e302fa8..d71f3b6 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -736,6 +736,7 @@
cc_test {
name: "hwui_unit_tests",
+ test_config: "tests/unit/AndroidTest.xml",
defaults: [
"hwui_test_defaults",
"android_graphics_apex",
@@ -803,6 +804,7 @@
cc_benchmark {
name: "hwuimacro",
+ test_config: "tests/macrobench/AndroidTest.xml",
defaults: ["hwui_test_defaults"],
static_libs: ["libhwui"],
@@ -822,6 +824,7 @@
cc_benchmark {
name: "hwuimicro",
+ test_config: "tests/microbench/AndroidTest.xml",
defaults: ["hwui_test_defaults"],
static_libs: ["libhwui_static"],
diff --git a/libs/hwui/AndroidTest.xml b/libs/hwui/tests/macrobench/AndroidTest.xml
similarity index 60%
copy from libs/hwui/AndroidTest.xml
copy to libs/hwui/tests/macrobench/AndroidTest.xml
index 75f61f5..5b8576d 100644
--- a/libs/hwui/AndroidTest.xml
+++ b/libs/hwui/tests/macrobench/AndroidTest.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- 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.
@@ -13,24 +13,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<configuration description="Config for hwuimicro">
+<configuration description="Config for hwuimacro">
<target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
<option name="cleanup" value="true" />
- <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" />
- <option name="push" value="hwuimicro->/data/local/tmp/benchmarktest/hwuimicro" />
<option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" />
</target_preparer>
<option name="test-suite-tag" value="apct" />
<option name="not-shardable" value="true" />
- <test class="com.android.tradefed.testtype.GTest" >
- <option name="native-test-device-path" value="/data/local/tmp/nativetest" />
- <option name="module-name" value="hwui_unit_tests" />
- </test>
- <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" >
- <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" />
- <option name="benchmark-module-name" value="hwuimicro" />
- <option name="file-exclusion-filter-regex" value=".*\.config$" />
- </test>
<test class="com.android.tradefed.testtype.GoogleBenchmarkTest" >
<option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" />
<option name="benchmark-module-name" value="hwuimacro" />
diff --git a/libs/hwui/tests/macrobench/how_to_run.txt b/libs/hwui/tests/macrobench/how_to_run.txt
index 3c3d36a..59ef25a 100644
--- a/libs/hwui/tests/macrobench/how_to_run.txt
+++ b/libs/hwui/tests/macrobench/how_to_run.txt
@@ -3,3 +3,7 @@
adb shell /data/benchmarktest/hwuimacro/hwuimacro shadowgrid2 --onscreen
Pass --help to get help
+
+OR (if you don't need to pass arguments)
+
+atest hwuimacro
diff --git a/libs/hwui/AndroidTest.xml b/libs/hwui/tests/microbench/AndroidTest.xml
similarity index 63%
rename from libs/hwui/AndroidTest.xml
rename to libs/hwui/tests/microbench/AndroidTest.xml
index 75f61f5..d67305df 100644
--- a/libs/hwui/AndroidTest.xml
+++ b/libs/hwui/tests/microbench/AndroidTest.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- 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.
@@ -16,24 +16,13 @@
<configuration description="Config for hwuimicro">
<target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
<option name="cleanup" value="true" />
- <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" />
<option name="push" value="hwuimicro->/data/local/tmp/benchmarktest/hwuimicro" />
- <option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" />
</target_preparer>
<option name="test-suite-tag" value="apct" />
<option name="not-shardable" value="true" />
- <test class="com.android.tradefed.testtype.GTest" >
- <option name="native-test-device-path" value="/data/local/tmp/nativetest" />
- <option name="module-name" value="hwui_unit_tests" />
- </test>
<test class="com.android.tradefed.testtype.GoogleBenchmarkTest" >
<option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" />
<option name="benchmark-module-name" value="hwuimicro" />
<option name="file-exclusion-filter-regex" value=".*\.config$" />
</test>
- <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" >
- <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" />
- <option name="benchmark-module-name" value="hwuimacro" />
- <option name="file-exclusion-filter-regex" value=".*\.config$" />
- </test>
</configuration>
diff --git a/libs/hwui/tests/microbench/how_to_run.txt b/libs/hwui/tests/microbench/how_to_run.txt
index 915fe5d..c7ddc1a 100755
--- a/libs/hwui/tests/microbench/how_to_run.txt
+++ b/libs/hwui/tests/microbench/how_to_run.txt
@@ -1,3 +1,7 @@
mmm -j8 frameworks/base/libs/hwui &&
adb push $OUT/data/benchmarktest/hwuimicro/hwuimicro /data/benchmarktest/hwuimicro/hwuimicro &&
adb shell /data/benchmarktest/hwuimicro/hwuimicro
+
+OR
+
+atest hwuimicro
diff --git a/libs/hwui/tests/unit/AndroidTest.xml b/libs/hwui/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..dc586c9
--- /dev/null
+++ b/libs/hwui/tests/unit/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Config for hwui_unit_tests">
+ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+ <option name="cleanup" value="true" />
+ <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" />
+ </target_preparer>
+ <option name="test-suite-tag" value="apct" />
+ <option name="not-shardable" value="true" />
+ <test class="com.android.tradefed.testtype.GTest" >
+ <option name="native-test-device-path" value="/data/local/tmp/nativetest" />
+ <option name="module-name" value="hwui_unit_tests" />
+ </test>
+</configuration>
diff --git a/libs/hwui/tests/unit/how_to_run.txt b/libs/hwui/tests/unit/how_to_run.txt
index c11d6eb3..1a35adf 100755
--- a/libs/hwui/tests/unit/how_to_run.txt
+++ b/libs/hwui/tests/unit/how_to_run.txt
@@ -2,3 +2,11 @@
adb push $ANDROID_PRODUCT_OUT/data/nativetest/hwui_unit_tests/hwui_unit_tests \
/data/nativetest/hwui_unit_tests/hwui_unit_tests &&
adb shell /data/nativetest/hwui_unit_tests/hwui_unit_tests
+
+OR
+
+atest hwui_unit_tests
+
+OR, if you need arguments, they can be passed as native-test-flags, as in:
+
+atest hwui_unit_tests -- --test-arg com.android.tradefed.testtype.GTest:native-test-flag:"--renderer=skiavk"
diff --git a/libs/hwui/tests/unit/main.cpp b/libs/hwui/tests/unit/main.cpp
index 76cbc8a..3fd15c4 100644
--- a/libs/hwui/tests/unit/main.cpp
+++ b/libs/hwui/tests/unit/main.cpp
@@ -15,6 +15,7 @@
*/
#include <getopt.h>
+#include <log/log.h>
#include <signal.h>
#include "Properties.h"
@@ -65,6 +66,19 @@
return RenderPipelineType::SkiaGL;
}
+static constexpr const char* renderPipelineTypeName(const RenderPipelineType renderPipelineType) {
+ switch (renderPipelineType) {
+ case RenderPipelineType::SkiaGL:
+ return "SkiaGL";
+ case RenderPipelineType::SkiaVulkan:
+ return "SkiaVulkan";
+ case RenderPipelineType::SkiaCpu:
+ return "SkiaCpu";
+ case RenderPipelineType::NotInitialized:
+ return "NotInitialized";
+ }
+}
+
struct Options {
RenderPipelineType renderer = RenderPipelineType::SkiaGL;
};
@@ -118,6 +132,7 @@
auto opts = parseOptions(argc, argv);
Properties::overrideRenderPipelineType(opts.renderer);
+ ALOGI("Starting HWUI unit tests with %s pipeline", renderPipelineTypeName(opts.renderer));
// Run the tests
testing::InitGoogleTest(&argc, argv);
diff --git a/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm b/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm
index 9c2064c..8c6880b 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm
@@ -58,7 +58,8 @@
label: '5'
base: '\u0665'
capslock: '5'
- shift: '%'
+ shift: '\u066a'
+ shift+capslock: '%'
}
key 6 {
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index ce997bf..5c4cdb2 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1010,8 +1010,8 @@
<!-- UI debug setting: force allow on external summary [CHAR LIMIT=150] -->
<string name="force_resizable_activities_summary">Make all activities resizable for multi-window, regardless of manifest values.</string>
- <!-- Title for a toggle that enables support for windows to be in freeform (apps run in resizable windows). [CHAR LIMIT=50] -->
- <string name="enable_freeform_support">Enable freeform window support</string>
+ <!-- Title for a toggle that enables support for windows to be in freeform. Freeform windows enables users to freely arrange and resize overlapping apps. [CHAR LIMIT=50] -->
+ <string name="enable_freeform_support">Enable freeform windows</string>
<!-- Local (desktop) backup password menu title [CHAR LIMIT=25] -->
<string name="local_backup_password_title">Desktop backup password</string>
@@ -1164,7 +1164,7 @@
<!-- [CHAR_LIMIT=40] Label for battery level chart when charging with duration -->
<string name="power_charging_duration"><xliff:g id="level">%1$s</xliff:g> - <xliff:g id="time">%2$s</xliff:g> left until full</string>
<!-- [CHAR_LIMIT=80] Label for battery level chart when charge been limited -->
- <string name="power_charging_limited"><xliff:g id="level">%1$s</xliff:g> - Charging optimized</string>
+ <string name="power_charging_limited"><xliff:g id="level">%1$s</xliff:g> - Charging on hold to protect battery</string>
<!-- [CHAR_LIMIT=80] Label for battery charging future pause -->
<string name="power_charging_future_paused"><xliff:g id="level">%1$s</xliff:g> - Charging</string>
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 3be5231..368085f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -32,6 +32,7 @@
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.ElementMatcher
@@ -70,7 +71,7 @@
}
object AllElements : ElementMatcher {
- override fun matches(key: ElementKey, scene: SceneKey) = true
+ override fun matches(key: ElementKey, content: ContentKey) = true
}
private object TransitionDuration {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index 859c036..df068c4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -92,7 +92,7 @@
fun SceneScope.Notifications(burnInParams: BurnInParameters?, modifier: Modifier = Modifier) {
val areNotificationsVisible by
lockscreenContentViewModel
- .areNotificationsVisible(sceneKey)
+ .areNotificationsVisible(contentKey)
.collectAsStateWithLifecycle(initialValue = false)
if (!areNotificationsVisible) {
return
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
new file mode 100644
index 0000000..4b3a39b
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.systemui.notifications.ui.composable
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.unit.IntOffset
+import com.android.compose.nestedscroll.PriorityNestedScrollConnection
+import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@Composable
+fun Modifier.stackVerticalOverscroll(
+ coroutineScope: CoroutineScope,
+ canScrollForward: () -> Boolean
+): Modifier {
+ val overscrollOffset = remember { Animatable(0f) }
+ val stackNestedScrollConnection = remember {
+ NotificationStackNestedScrollConnection(
+ stackOffset = { overscrollOffset.value },
+ canScrollForward = canScrollForward,
+ onScroll = { offsetAvailable ->
+ coroutineScope.launch {
+ overscrollOffset.snapTo(overscrollOffset.value + offsetAvailable * 0.3f)
+ }
+ },
+ onStop = { velocityAvailable ->
+ coroutineScope.launch {
+ overscrollOffset.animateTo(
+ targetValue = 0f,
+ initialVelocity = velocityAvailable,
+ animationSpec = tween()
+ )
+ }
+ }
+ )
+ }
+
+ return this.then(
+ Modifier.nestedScroll(stackNestedScrollConnection).offset {
+ IntOffset(x = 0, y = overscrollOffset.value.roundToInt())
+ }
+ )
+}
+
+fun NotificationStackNestedScrollConnection(
+ stackOffset: () -> Float,
+ canScrollForward: () -> Boolean,
+ onStart: (Float) -> Unit = {},
+ onScroll: (Float) -> Unit,
+ onStop: (Float) -> Unit = {},
+): PriorityNestedScrollConnection {
+ return PriorityNestedScrollConnection(
+ orientation = Orientation.Vertical,
+ canStartPreScroll = { _, _ -> false },
+ canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
+ offsetAvailable < 0f && offsetBeforeStart < 0f && !canScrollForward()
+ },
+ canStartPostFling = { velocityAvailable -> velocityAvailable < 0f && !canScrollForward() },
+ canContinueScroll = { source ->
+ if (source == NestedScrollSource.SideEffect) {
+ stackOffset() > STACK_OVERSCROLL_FLING_MIN_OFFSET
+ } else {
+ true
+ }
+ },
+ canScrollOnFling = true,
+ onStart = { offsetAvailable -> onStart(offsetAvailable) },
+ onScroll = { offsetAvailable ->
+ onScroll(offsetAvailable)
+ offsetAvailable
+ },
+ onStop = { velocityAvailable ->
+ onStop(velocityAvailable)
+ velocityAvailable
+ },
+ )
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 76a7a10..2eb7b3f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -474,6 +474,7 @@
.thenIf(shadeMode == ShadeMode.Single) {
Modifier.nestedScroll(scrimNestedScrollConnection)
}
+ .stackVerticalOverscroll(coroutineScope) { scrollState.canScrollForward }
.verticalScroll(scrollState)
.padding(top = topPadding)
.fillMaxWidth()
@@ -671,3 +672,4 @@
private val DEBUG_BOX_COLOR = Color(0f, 1f, 0f, 0.2f)
private const val HUN_SNOOZE_POSITIONAL_THRESHOLD_FRACTION = 0.25f
private const val HUN_SNOOZE_VELOCITY_THRESHOLD = -70f
+internal const val STACK_OVERSCROLL_FLING_MIN_OFFSET = -100f
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
index 114dcf4..afbc8e7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -67,15 +67,15 @@
/**
* Animate a scene Int value.
*
- * @see SceneScope.animateSceneValueAsState
+ * @see ContentScope.animateContentValueAsState
*/
@Composable
-fun SceneScope.animateSceneIntAsState(
+fun ContentScope.animateContentIntAsState(
value: Int,
key: ValueKey,
canOverflow: Boolean = true,
): AnimatedState<Int> {
- return animateSceneValueAsState(value, key, SharedIntType, canOverflow)
+ return animateContentValueAsState(value, key, SharedIntType, canOverflow)
}
/**
@@ -107,17 +107,28 @@
/**
* Animate a scene Float value.
*
- * @see SceneScope.animateSceneValueAsState
+ * @see ContentScope.animateContentValueAsState
*/
@Composable
-fun SceneScope.animateSceneFloatAsState(
+fun ContentScope.animateContentFloatAsState(
value: Float,
key: ValueKey,
canOverflow: Boolean = true,
): AnimatedState<Float> {
- return animateSceneValueAsState(value, key, SharedFloatType, canOverflow)
+ return animateContentValueAsState(value, key, SharedFloatType, canOverflow)
}
+@Deprecated(
+ "Use animateSceneFloatAsState() instead",
+ replaceWith = ReplaceWith("animateContentFloatAsState(value, key, canOverflow)")
+)
+@Composable
+fun ContentScope.animateSceneFloatAsState(
+ value: Float,
+ key: ValueKey,
+ canOverflow: Boolean = true,
+) = animateContentFloatAsState(value, key, canOverflow)
+
/**
* Animate a shared element Float value.
*
@@ -147,17 +158,28 @@
/**
* Animate a scene Dp value.
*
- * @see SceneScope.animateSceneValueAsState
+ * @see ContentScope.animateContentValueAsState
*/
@Composable
-fun SceneScope.animateSceneDpAsState(
+fun ContentScope.animateContentDpAsState(
value: Dp,
key: ValueKey,
canOverflow: Boolean = true,
): AnimatedState<Dp> {
- return animateSceneValueAsState(value, key, SharedDpType, canOverflow)
+ return animateContentValueAsState(value, key, SharedDpType, canOverflow)
}
+@Deprecated(
+ "Use animateSceneDpAsState() instead",
+ replaceWith = ReplaceWith("animateContentDpAsState(value, key, canOverflow)")
+)
+@Composable
+fun ContentScope.animateSceneDpAsState(
+ value: Dp,
+ key: ValueKey,
+ canOverflow: Boolean = true,
+) = animateContentDpAsState(value, key, canOverflow)
+
/**
* Animate a shared element Dp value.
*
@@ -188,14 +210,14 @@
/**
* Animate a scene Color value.
*
- * @see SceneScope.animateSceneValueAsState
+ * @see ContentScope.animateContentValueAsState
*/
@Composable
-fun SceneScope.animateSceneColorAsState(
+fun ContentScope.animateContentColorAsState(
value: Color,
key: ValueKey,
): AnimatedState<Color> {
- return animateSceneValueAsState(value, key, SharedColorType, canOverflow = false)
+ return animateContentValueAsState(value, key, SharedColorType, canOverflow = false)
}
/**
@@ -261,24 +283,24 @@
@Composable
internal fun <T> animateSharedValueAsState(
layoutImpl: SceneTransitionLayoutImpl,
- scene: SceneKey,
+ content: ContentKey,
element: ElementKey?,
key: ValueKey,
value: T,
type: SharedValueType<T, *>,
canOverflow: Boolean,
): AnimatedState<T> {
- DisposableEffect(layoutImpl, scene, element, key) {
- // Create the associated maps that hold the current value for each (element, scene) pair.
+ DisposableEffect(layoutImpl, content, element, key) {
+ // Create the associated maps that hold the current value for each (element, content) pair.
val valueMap = layoutImpl.sharedValues.getOrPut(key) { mutableMapOf() }
val sharedValue = valueMap.getOrPut(element) { SharedValue(type) } as SharedValue<T, *>
val targetValues = sharedValue.targetValues
- targetValues[scene] = value
+ targetValues[content] = value
onDispose {
// Remove the value associated to the current scene, and eventually remove the maps if
// they are empty.
- targetValues.remove(scene)
+ targetValues.remove(content)
if (targetValues.isEmpty() && valueMap[element] === sharedValue) {
valueMap.remove(element)
@@ -297,11 +319,11 @@
error("value is equal to $value, which is the undefined value for this type.")
}
- sharedValue<T, Any>(layoutImpl, key, element).targetValues[scene] = value
+ sharedValue<T, Any>(layoutImpl, key, element).targetValues[content] = value
}
- return remember(layoutImpl, scene, element, canOverflow) {
- AnimatedStateImpl<T, Any>(layoutImpl, scene, element, key, canOverflow)
+ return remember(layoutImpl, content, element, canOverflow) {
+ AnimatedStateImpl<T, Any>(layoutImpl, content, element, key, canOverflow)
}
}
@@ -322,8 +344,8 @@
internal class SharedValue<T, Delta>(
val type: SharedValueType<T, Delta>,
) {
- /** The target value of this shared value for each scene. */
- val targetValues = SnapshotStateMap<SceneKey, T>()
+ /** The target value of this shared value for each content. */
+ val targetValues = SnapshotStateMap<ContentKey, T>()
/** The last value of this shared value. */
var lastValue: T = type.unspecifiedValue
@@ -340,7 +362,7 @@
private class AnimatedStateImpl<T, Delta>(
private val layoutImpl: SceneTransitionLayoutImpl,
- private val scene: SceneKey,
+ private val content: ContentKey,
private val element: ElementKey?,
private val key: ValueKey,
private val canOverflow: Boolean,
@@ -356,14 +378,14 @@
// TODO(b/311600838): Remove this. We should not have to fallback to the current
// scene value, but we have to because code of removed nodes can still run if they
// are placed with a graphics layer.
- ?: sharedValue[scene]
+ ?: sharedValue[content]
?: error(valueReadTooEarlyMessage(key))
val interruptedValue = computeInterruptedValue(sharedValue, transition, value)
sharedValue.lastValue = interruptedValue
return interruptedValue
}
- private operator fun SharedValue<T, *>.get(scene: SceneKey): T? = targetValues[scene]
+ private operator fun SharedValue<T, *>.get(content: ContentKey): T? = targetValues[content]
private fun valueOrNull(
sharedValue: SharedValue<T, *>,
@@ -401,7 +423,7 @@
val targetValues = sharedValue.targetValues
val transition =
if (element != null) {
- layoutImpl.elements[element]?.sceneStates?.let { sceneStates ->
+ layoutImpl.elements[element]?.stateByContent?.let { sceneStates ->
layoutImpl.state.currentTransitions.fastLastOrNull { transition ->
transition.fromScene in sceneStates || transition.toScene in sceneStates
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index fb13b57..67d1b59 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -30,6 +30,7 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import com.android.compose.animation.scene.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
+import com.android.compose.animation.scene.content.Scene
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 3ad07d0..0b5e58f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -48,6 +48,7 @@
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastLastOrNull
import androidx.compose.ui.util.lerp
+import com.android.compose.animation.scene.content.Content
import com.android.compose.animation.scene.transformation.PropertyTransformation
import com.android.compose.animation.scene.transformation.SharedElementTransformation
import com.android.compose.ui.util.lerp
@@ -57,30 +58,30 @@
/** An element on screen, that can be composed in one or more scenes. */
@Stable
internal class Element(val key: ElementKey) {
- /** The mapping between a scene and the state this element has in that scene, if any. */
+ /** The mapping between a content and the state this element has in that content, if any. */
// TODO(b/316901148): Make this a normal map instead once we can make sure that new transitions
// are first seen by composition then layout/drawing code. See b/316901148#comment2 for details.
- val sceneStates = SnapshotStateMap<SceneKey, SceneState>()
+ val stateByContent = SnapshotStateMap<ContentKey, State>()
/**
* The last transition that was used when computing the state (size, position and alpha) of this
- * element in any scene, or `null` if it was last laid out when idle.
+ * element in any content, or `null` if it was last laid out when idle.
*/
var lastTransition: TransitionState.Transition? = null
- /** Whether this element was ever drawn in a scene. */
- var wasDrawnInAnyScene = false
+ /** Whether this element was ever drawn in a content. */
+ var wasDrawnInAnyContent = false
override fun toString(): String {
return "Element(key=$key)"
}
- /** The last and target state of this element in a given scene. */
+ /** The last and target state of this element in a given content. */
@Stable
- class SceneState(val scene: SceneKey) {
+ class State(val content: ContentKey) {
/**
- * The *target* state of this element in this scene, i.e. the state of this element when we
- * are idle on this scene.
+ * The *target* state of this element in this content, i.e. the state of this element when
+ * we are idle on this content.
*/
var targetSize by mutableStateOf(SizeUnspecified)
var targetOffset by mutableStateOf(Offset.Unspecified)
@@ -91,7 +92,9 @@
var lastScale = Scale.Unspecified
var lastAlpha = AlphaUnspecified
- /** The state of this element in this scene right before the last interruption (if any). */
+ /**
+ * The state of this element in this content right before the last interruption (if any).
+ */
var offsetBeforeInterruption = Offset.Unspecified
var sizeBeforeInterruption = SizeUnspecified
var scaleBeforeInterruption = Scale.Unspecified
@@ -109,7 +112,7 @@
var alphaInterruptionDelta = 0f
/**
- * The attached [ElementNode] a Modifier.element() for a given element and scene. During
+ * The attached [ElementNode] a Modifier.element() for a given element and content. During
* composition, this set could have 0 to 2 elements. After composition and after all
* modifier nodes have been attached/detached, this set should contain exactly 1 element.
*/
@@ -130,19 +133,19 @@
}
}
-/** The implementation of [SceneScope.element]. */
+/** The implementation of [ContentScope.element]. */
@Stable
internal fun Modifier.element(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
+ content: Content,
key: ElementKey,
): Modifier {
// Make sure that we read the current transitions during composition and not during
// layout/drawing.
// TODO(b/341072461): Revert this and read the current transitions in ElementNode directly once
- // we can ensure that SceneTransitionLayoutImpl will compose new scenes first.
+ // we can ensure that SceneTransitionLayoutImpl will compose new contents first.
val currentTransitions = layoutImpl.state.currentTransitions
- return then(ElementModifier(layoutImpl, currentTransitions, scene, key)).testTag(key.testTag)
+ return then(ElementModifier(layoutImpl, currentTransitions, content, key)).testTag(key.testTag)
}
/**
@@ -152,92 +155,92 @@
private data class ElementModifier(
private val layoutImpl: SceneTransitionLayoutImpl,
private val currentTransitions: List<TransitionState.Transition>,
- private val scene: Scene,
+ private val content: Content,
private val key: ElementKey,
) : ModifierNodeElement<ElementNode>() {
- override fun create(): ElementNode = ElementNode(layoutImpl, currentTransitions, scene, key)
+ override fun create(): ElementNode = ElementNode(layoutImpl, currentTransitions, content, key)
override fun update(node: ElementNode) {
- node.update(layoutImpl, currentTransitions, scene, key)
+ node.update(layoutImpl, currentTransitions, content, key)
}
}
internal class ElementNode(
private var layoutImpl: SceneTransitionLayoutImpl,
private var currentTransitions: List<TransitionState.Transition>,
- private var scene: Scene,
+ private var content: Content,
private var key: ElementKey,
) : Modifier.Node(), DrawModifierNode, ApproachLayoutModifierNode, TraversableNode {
private var _element: Element? = null
private val element: Element
get() = _element!!
- private var _sceneState: Element.SceneState? = null
- private val sceneState: Element.SceneState
- get() = _sceneState!!
+ private var _stateInContent: Element.State? = null
+ private val stateInContent: Element.State
+ get() = _stateInContent!!
override val traverseKey: Any = ElementTraverseKey
override fun onAttach() {
super.onAttach()
- updateElementAndSceneValues()
- addNodeToSceneState()
+ updateElementAndContentValues()
+ addNodeToContentState()
}
- private fun updateElementAndSceneValues() {
+ private fun updateElementAndContentValues() {
val element =
layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
_element = element
- _sceneState =
- element.sceneStates[scene.key]
- ?: Element.SceneState(scene.key).also { element.sceneStates[scene.key] = it }
+ _stateInContent =
+ element.stateByContent[content.key]
+ ?: Element.State(content.key).also { element.stateByContent[content.key] = it }
}
- private fun addNodeToSceneState() {
- sceneState.nodes.add(this)
+ private fun addNodeToContentState() {
+ stateInContent.nodes.add(this)
coroutineScope.launch {
// At this point all [CodeLocationNode] have been attached or detached, which means that
- // [sceneState.codeLocations] should have exactly 1 element, otherwise this means that
- // this element was composed multiple times in the same scene.
- val nCodeLocations = sceneState.nodes.size
- if (nCodeLocations != 1 || !sceneState.nodes.contains(this@ElementNode)) {
- error("$key was composed $nCodeLocations times in ${sceneState.scene}")
+ // [elementState.codeLocations] should have exactly 1 element, otherwise this means that
+ // this element was composed multiple times in the same content.
+ val nCodeLocations = stateInContent.nodes.size
+ if (nCodeLocations != 1 || !stateInContent.nodes.contains(this@ElementNode)) {
+ error("$key was composed $nCodeLocations times in ${stateInContent.content}")
}
}
}
override fun onDetach() {
super.onDetach()
- removeNodeFromSceneState()
- maybePruneMaps(layoutImpl, element, sceneState)
+ removeNodeFromContentState()
+ maybePruneMaps(layoutImpl, element, stateInContent)
_element = null
- _sceneState = null
+ _stateInContent = null
}
- private fun removeNodeFromSceneState() {
- sceneState.nodes.remove(this)
+ private fun removeNodeFromContentState() {
+ stateInContent.nodes.remove(this)
}
fun update(
layoutImpl: SceneTransitionLayoutImpl,
currentTransitions: List<TransitionState.Transition>,
- scene: Scene,
+ content: Content,
key: ElementKey,
) {
- check(layoutImpl == this.layoutImpl && scene == this.scene)
+ check(layoutImpl == this.layoutImpl && content == this.content)
this.currentTransitions = currentTransitions
- removeNodeFromSceneState()
+ removeNodeFromContentState()
val prevElement = this.element
- val prevSceneState = this.sceneState
+ val prevElementState = this.stateInContent
this.key = key
- updateElementAndSceneValues()
+ updateElementAndContentValues()
- addNodeToSceneState()
- maybePruneMaps(layoutImpl, prevElement, prevSceneState)
+ addNodeToContentState()
+ maybePruneMaps(layoutImpl, prevElement, prevElementState)
}
override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
@@ -262,15 +265,15 @@
check(isLookingAhead)
return measurable.measure(constraints).run {
- // Update the size this element has in this scene when idle.
- sceneState.targetSize = size()
+ // Update the size this element has in this content when idle.
+ stateInContent.targetSize = size()
layout(width, height) {
// Update the offset (relative to the SceneTransitionLayout) this element has in
- // this scene when idle.
+ // this content when idle.
coordinates?.let { coords ->
with(layoutImpl.lookaheadScope) {
- sceneState.targetOffset =
+ stateInContent.targetOffset =
lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
}
}
@@ -287,22 +290,22 @@
val transition = elementTransition(layoutImpl, element, transitions)
// If this element is not supposed to be laid out now, either because it is not part of any
- // ongoing transition or the other scene of its transition is overscrolling, then lay out
+ // ongoing transition or the other content of its transition is overscrolling, then lay out
// the element normally and don't place it.
val overscrollScene = transition?.currentOverscrollSpec?.scene
- val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != scene.key
+ val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != content.key
val isNotPartOfAnyOngoingTransitions = transitions.isNotEmpty() && transition == null
if (isNotPartOfAnyOngoingTransitions || isOtherSceneOverscrolling) {
recursivelyClearPlacementValues()
- sceneState.lastSize = Element.SizeUnspecified
+ stateInContent.lastSize = Element.SizeUnspecified
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) { /* Do not place */ }
}
val placeable =
- measure(layoutImpl, element, transition, sceneState, measurable, constraints)
- sceneState.lastSize = placeable.size()
+ measure(layoutImpl, element, transition, stateInContent, measurable, constraints)
+ stateInContent.lastSize = placeable.size()
return layout(placeable.width, placeable.height) { place(transition, placeable) }
}
@@ -312,12 +315,12 @@
) {
with(layoutImpl.lookaheadScope) {
// Update the offset (relative to the SceneTransitionLayout) this element has in this
- // scene when idle.
+ // content when idle.
val coords =
coordinates ?: error("Element ${element.key} does not have any coordinates")
- // No need to place the element in this scene if we don't want to draw it anyways.
- if (!shouldPlaceElement(layoutImpl, scene.key, element, transition)) {
+ // No need to place the element in this content if we don't want to draw it anyways.
+ if (!shouldPlaceElement(layoutImpl, content.key, element, transition)) {
recursivelyClearPlacementValues()
return
}
@@ -326,10 +329,10 @@
val targetOffset =
computeValue(
layoutImpl,
- sceneState,
+ stateInContent,
element,
transition,
- sceneValue = { it.targetOffset },
+ contentValue = { it.targetOffset },
transformation = { it.offset },
currentValue = { currentOffset },
isSpecified = { it != Offset.Unspecified },
@@ -343,17 +346,17 @@
value = targetOffset,
unspecifiedValue = Offset.Unspecified,
zeroValue = Offset.Zero,
- getValueBeforeInterruption = { sceneState.offsetBeforeInterruption },
- setValueBeforeInterruption = { sceneState.offsetBeforeInterruption = it },
- getInterruptionDelta = { sceneState.offsetInterruptionDelta },
+ getValueBeforeInterruption = { stateInContent.offsetBeforeInterruption },
+ setValueBeforeInterruption = { stateInContent.offsetBeforeInterruption = it },
+ getInterruptionDelta = { stateInContent.offsetInterruptionDelta },
setInterruptionDelta = { delta ->
setPlacementInterruptionDelta(
element = element,
- sceneState = sceneState,
+ stateInContent = stateInContent,
transition = transition,
delta = delta,
- setter = { sceneState, delta ->
- sceneState.offsetInterruptionDelta = delta
+ setter = { stateInContent, delta ->
+ stateInContent.offsetInterruptionDelta = delta
},
)
},
@@ -361,14 +364,15 @@
add = { a, b, bProgress -> a + b * bProgress },
)
- sceneState.lastOffset = interruptedOffset
+ stateInContent.lastOffset = interruptedOffset
val offset = (interruptedOffset - currentOffset).round()
if (
- isElementOpaque(scene, element, transition) &&
- interruptedAlpha(layoutImpl, element, transition, sceneState, alpha = 1f) == 1f
+ isElementOpaque(content, element, transition) &&
+ interruptedAlpha(layoutImpl, element, transition, stateInContent, alpha = 1f) ==
+ 1f
) {
- sceneState.lastAlpha = 1f
+ stateInContent.lastAlpha = 1f
// TODO(b/291071158): Call placeWithLayer() if offset != IntOffset.Zero and size is
// not animated once b/305195729 is fixed. Test that drawing is not invalidated in
@@ -387,11 +391,11 @@
}
val transition = elementTransition(layoutImpl, element, currentTransitions)
- if (!shouldPlaceElement(layoutImpl, scene.key, element, transition)) {
+ if (!shouldPlaceElement(layoutImpl, content.key, element, transition)) {
return@placeWithLayer
}
- alpha = elementAlpha(layoutImpl, element, transition, sceneState)
+ alpha = elementAlpha(layoutImpl, element, transition, stateInContent)
compositingStrategy = CompositingStrategy.ModulateAlpha
}
}
@@ -404,24 +408,24 @@
* for the descendants for which approachMeasure() won't be called.
*/
private fun recursivelyClearPlacementValues() {
- fun Element.SceneState.clearLastPlacementValues() {
+ fun Element.State.clearLastPlacementValues() {
lastOffset = Offset.Unspecified
lastScale = Scale.Unspecified
lastAlpha = Element.AlphaUnspecified
}
- sceneState.clearLastPlacementValues()
+ stateInContent.clearLastPlacementValues()
traverseDescendants(ElementTraverseKey) { node ->
- (node as ElementNode)._sceneState?.clearLastPlacementValues()
+ (node as ElementNode)._stateInContent?.clearLastPlacementValues()
TraversableNode.Companion.TraverseDescendantsAction.ContinueTraversal
}
}
override fun ContentDrawScope.draw() {
- element.wasDrawnInAnyScene = true
+ element.wasDrawnInAnyContent = true
val transition = elementTransition(layoutImpl, element, currentTransitions)
- val drawScale = getDrawScale(layoutImpl, element, transition, sceneState)
+ val drawScale = getDrawScale(layoutImpl, element, transition, stateInContent)
if (drawScale == Scale.Default) {
drawContent()
} else {
@@ -441,16 +445,21 @@
private fun maybePruneMaps(
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
- sceneState: Element.SceneState,
+ stateInContent: Element.State,
) {
- // If element is not composed from this scene anymore, remove the scene values. This
+ // If element is not composed in this content anymore, remove the content values. This
// works because [onAttach] is called before [onDetach], so if an element is moved from
// the UI tree we will first add the new code location then remove the old one.
- if (sceneState.nodes.isEmpty() && element.sceneStates[sceneState.scene] == sceneState) {
- element.sceneStates.remove(sceneState.scene)
+ if (
+ stateInContent.nodes.isEmpty() &&
+ element.stateByContent[stateInContent.content] == stateInContent
+ ) {
+ element.stateByContent.remove(stateInContent.content)
- // If the element is not composed in any scene, remove it from the elements map.
- if (element.sceneStates.isEmpty() && layoutImpl.elements[element.key] == element) {
+ // If the element is not composed in any content, remove it from the elements map.
+ if (
+ element.stateByContent.isEmpty() && layoutImpl.elements[element.key] == element
+ ) {
layoutImpl.elements.remove(element.key)
}
}
@@ -460,7 +469,7 @@
/**
* The transition that we should consider for [element]. This is the last transition where one of
- * its scenes contains the element.
+ * its contents contains the element.
*/
private fun elementTransition(
layoutImpl: SceneTransitionLayoutImpl,
@@ -469,7 +478,8 @@
): TransitionState.Transition? {
val transition =
transitions.fastLastOrNull { transition ->
- transition.fromScene in element.sceneStates || transition.toScene in element.sceneStates
+ transition.fromScene in element.stateByContent ||
+ transition.toScene in element.stateByContent
}
val previousTransition = element.lastTransition
@@ -480,7 +490,7 @@
prepareInterruption(layoutImpl, element, transition, previousTransition)
} else if (transition == null && previousTransition != null) {
// The transition was just finished.
- element.sceneStates.values.forEach {
+ element.stateByContent.values.forEach {
it.clearValuesBeforeInterruption()
it.clearInterruptionDeltas()
}
@@ -499,32 +509,32 @@
return
}
- val sceneStates = element.sceneStates
- fun updatedSceneState(key: SceneKey): Element.SceneState? {
- return sceneStates[key]?.also { it.selfUpdateValuesBeforeInterruption() }
+ val stateByContent = element.stateByContent
+ fun updateStateInContent(key: ContentKey): Element.State? {
+ return stateByContent[key]?.also { it.selfUpdateValuesBeforeInterruption() }
}
- val previousFromState = updatedSceneState(previousTransition.fromScene)
- val previousToState = updatedSceneState(previousTransition.toScene)
- val fromState = updatedSceneState(transition.fromScene)
- val toState = updatedSceneState(transition.toScene)
+ val previousFromState = updateStateInContent(previousTransition.fromScene)
+ val previousToState = updateStateInContent(previousTransition.toScene)
+ val fromState = updateStateInContent(transition.fromScene)
+ val toState = updateStateInContent(transition.toScene)
reconcileStates(element, previousTransition)
reconcileStates(element, transition)
- // Remove the interruption values to all scenes but the scene(s) where the element will be
+ // Remove the interruption values to all contents but the content(s) where the element will be
// placed, to make sure that interruption deltas are computed only right after this interruption
// is prepared.
- fun cleanInterruptionValues(sceneState: Element.SceneState) {
- sceneState.sizeInterruptionDelta = IntSize.Zero
- sceneState.offsetInterruptionDelta = Offset.Zero
- sceneState.alphaInterruptionDelta = 0f
- sceneState.scaleInterruptionDelta = Scale.Zero
+ fun cleanInterruptionValues(stateInContent: Element.State) {
+ stateInContent.sizeInterruptionDelta = IntSize.Zero
+ stateInContent.offsetInterruptionDelta = Offset.Zero
+ stateInContent.alphaInterruptionDelta = 0f
+ stateInContent.scaleInterruptionDelta = Scale.Zero
- if (!shouldPlaceElement(layoutImpl, sceneState.scene, element, transition)) {
- sceneState.offsetBeforeInterruption = Offset.Unspecified
- sceneState.alphaBeforeInterruption = Element.AlphaUnspecified
- sceneState.scaleBeforeInterruption = Scale.Unspecified
+ if (!shouldPlaceElement(layoutImpl, stateInContent.content, element, transition)) {
+ stateInContent.offsetBeforeInterruption = Offset.Unspecified
+ stateInContent.alphaBeforeInterruption = Element.AlphaUnspecified
+ stateInContent.scaleBeforeInterruption = Scale.Unspecified
}
}
@@ -542,8 +552,8 @@
element: Element,
transition: TransitionState.Transition,
) {
- val fromSceneState = element.sceneStates[transition.fromScene] ?: return
- val toSceneState = element.sceneStates[transition.toScene] ?: return
+ val fromSceneState = element.stateByContent[transition.fromScene] ?: return
+ val toSceneState = element.stateByContent[transition.toScene] ?: return
if (!isSharedElementEnabled(element.key, transition)) {
return
}
@@ -563,7 +573,7 @@
}
}
-private fun Element.SceneState.selfUpdateValuesBeforeInterruption() {
+private fun Element.State.selfUpdateValuesBeforeInterruption() {
sizeBeforeInterruption = lastSize
if (lastAlpha > 0f) {
@@ -571,7 +581,7 @@
scaleBeforeInterruption = lastScale
alphaBeforeInterruption = lastAlpha
} else {
- // Consider the element as not placed in this scene if it was fully transparent.
+ // Consider the element as not placed in this content if it was fully transparent.
// TODO(b/290930950): Look into using derived state inside place() instead to not even place
// the element at all when alpha == 0f.
offsetBeforeInterruption = Offset.Unspecified
@@ -580,7 +590,7 @@
}
}
-private fun Element.SceneState.updateValuesBeforeInterruption(lastState: Element.SceneState) {
+private fun Element.State.updateValuesBeforeInterruption(lastState: Element.State) {
offsetBeforeInterruption = lastState.offsetBeforeInterruption
sizeBeforeInterruption = lastState.sizeBeforeInterruption
scaleBeforeInterruption = lastState.scaleBeforeInterruption
@@ -589,14 +599,14 @@
clearInterruptionDeltas()
}
-private fun Element.SceneState.clearInterruptionDeltas() {
+private fun Element.State.clearInterruptionDeltas() {
offsetInterruptionDelta = Offset.Zero
sizeInterruptionDelta = IntSize.Zero
scaleInterruptionDelta = Scale.Zero
alphaInterruptionDelta = 0f
}
-private fun Element.SceneState.clearValuesBeforeInterruption() {
+private fun Element.State.clearValuesBeforeInterruption() {
offsetBeforeInterruption = Offset.Unspecified
scaleBeforeInterruption = Scale.Unspecified
alphaBeforeInterruption = Element.AlphaUnspecified
@@ -655,13 +665,13 @@
*/
private inline fun <T> setPlacementInterruptionDelta(
element: Element,
- sceneState: Element.SceneState,
+ stateInContent: Element.State,
transition: TransitionState.Transition?,
delta: T,
- setter: (Element.SceneState, T) -> Unit,
+ setter: (Element.State, T) -> Unit,
) {
- // Set the interruption delta on the current scene.
- setter(sceneState, delta)
+ // Set the interruption delta on the current content.
+ setter(stateInContent, delta)
if (transition == null) {
return
@@ -670,8 +680,9 @@
// If the element is shared, also set the delta on the other scene so that it is used by that
// scene if we start overscrolling it and change the scene where the element is placed.
val otherScene =
- if (sceneState.scene == transition.fromScene) transition.toScene else transition.fromScene
- val otherSceneState = element.sceneStates[otherScene] ?: return
+ if (stateInContent.content == transition.fromScene) transition.toScene
+ else transition.fromScene
+ val otherSceneState = element.stateByContent[otherScene] ?: return
if (isSharedElementEnabled(element.key, transition)) {
setter(otherSceneState, delta)
}
@@ -679,7 +690,7 @@
private fun shouldPlaceElement(
layoutImpl: SceneTransitionLayoutImpl,
- scene: SceneKey,
+ content: ContentKey,
element: Element,
transition: TransitionState.Transition?,
): Boolean {
@@ -688,15 +699,16 @@
return true
}
- // Don't place the element in this scene if this scene is not part of the current element
+ // Don't place the element in this content if this content is not part of the current element
// transition.
- if (scene != transition.fromScene && scene != transition.toScene) {
+ if (content != transition.fromScene && content != transition.toScene) {
return false
}
// Place the element if it is not shared.
if (
- transition.fromScene !in element.sceneStates || transition.toScene !in element.sceneStates
+ transition.fromScene !in element.stateByContent ||
+ transition.toScene !in element.stateByContent
) {
return true
}
@@ -708,7 +720,7 @@
return shouldPlaceOrComposeSharedElement(
layoutImpl,
- scene,
+ content,
element.key,
transition,
)
@@ -716,14 +728,14 @@
internal fun shouldPlaceOrComposeSharedElement(
layoutImpl: SceneTransitionLayoutImpl,
- scene: SceneKey,
+ content: ContentKey,
element: ElementKey,
transition: TransitionState.Transition,
): Boolean {
// If we are overscrolling, only place/compose the element in the overscrolling scene.
val overscrollScene = transition.currentOverscrollSpec?.scene
if (overscrollScene != null) {
- return scene == overscrollScene
+ return content == overscrollScene
}
val scenePicker = element.scenePicker
@@ -738,7 +750,7 @@
toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex,
) ?: return false
- return pickedScene == scene
+ return pickedScene == content
}
private fun isSharedElementEnabled(
@@ -775,7 +787,7 @@
* placement and we don't want to read the transition progress in that phase.
*/
private fun isElementOpaque(
- scene: Scene,
+ content: Content,
element: Element,
transition: TransitionState.Transition?,
): Boolean {
@@ -785,8 +797,8 @@
val fromScene = transition.fromScene
val toScene = transition.toScene
- val fromState = element.sceneStates[fromScene]
- val toState = element.sceneStates[toScene]
+ val fromState = element.stateByContent[fromScene]
+ val toState = element.stateByContent[toScene]
if (fromState == null && toState == null) {
// TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
@@ -799,7 +811,7 @@
return true
}
- return transition.transformationSpec.transformations(element.key, scene.key).alpha == null
+ return transition.transformationSpec.transformations(element.key, content.key).alpha == null
}
/**
@@ -814,15 +826,15 @@
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
transition: TransitionState.Transition?,
- sceneState: Element.SceneState,
+ stateInContent: Element.State,
): Float {
val alpha =
computeValue(
layoutImpl,
- sceneState,
+ stateInContent,
element,
transition,
- sceneValue = { 1f },
+ contentValue = { 1f },
transformation = { it.alpha },
currentValue = { 1f },
isSpecified = { true },
@@ -832,12 +844,12 @@
// If the element is fading during this transition and that it is drawn for the first time, make
// sure that it doesn't instantly appear on screen.
- if (!element.wasDrawnInAnyScene && alpha > 0f) {
- element.sceneStates.forEach { it.value.alphaBeforeInterruption = 0f }
+ if (!element.wasDrawnInAnyContent && alpha > 0f) {
+ element.stateByContent.forEach { it.value.alphaBeforeInterruption = 0f }
}
- val interruptedAlpha = interruptedAlpha(layoutImpl, element, transition, sceneState, alpha)
- sceneState.lastAlpha = interruptedAlpha
+ val interruptedAlpha = interruptedAlpha(layoutImpl, element, transition, stateInContent, alpha)
+ stateInContent.lastAlpha = interruptedAlpha
return interruptedAlpha
}
@@ -845,7 +857,7 @@
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
transition: TransitionState.Transition?,
- sceneState: Element.SceneState,
+ stateInContent: Element.State,
alpha: Float,
): Float {
return computeInterruptedValue(
@@ -854,16 +866,16 @@
value = alpha,
unspecifiedValue = Element.AlphaUnspecified,
zeroValue = 0f,
- getValueBeforeInterruption = { sceneState.alphaBeforeInterruption },
- setValueBeforeInterruption = { sceneState.alphaBeforeInterruption = it },
- getInterruptionDelta = { sceneState.alphaInterruptionDelta },
+ getValueBeforeInterruption = { stateInContent.alphaBeforeInterruption },
+ setValueBeforeInterruption = { stateInContent.alphaBeforeInterruption = it },
+ getInterruptionDelta = { stateInContent.alphaInterruptionDelta },
setInterruptionDelta = { delta ->
setPlacementInterruptionDelta(
element = element,
- sceneState = sceneState,
+ stateInContent = stateInContent,
transition = transition,
delta = delta,
- setter = { sceneState, delta -> sceneState.alphaInterruptionDelta = delta },
+ setter = { stateInContent, delta -> stateInContent.alphaInterruptionDelta = delta },
)
},
diff = { a, b -> a - b },
@@ -875,7 +887,7 @@
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
transition: TransitionState.Transition?,
- sceneState: Element.SceneState,
+ stateInContent: Element.State,
measurable: Measurable,
constraints: Constraints,
): Placeable {
@@ -887,10 +899,10 @@
val targetSize =
computeValue(
layoutImpl,
- sceneState,
+ stateInContent,
element,
transition,
- sceneValue = { it.targetSize },
+ contentValue = { it.targetSize },
transformation = { it.size },
currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
isSpecified = { it != Element.SizeUnspecified },
@@ -900,8 +912,8 @@
// The measurable was already measured, so we can't take interruptions into account here given
// that we are not allowed to measure the same measurable twice.
maybePlaceable?.let { placeable ->
- sceneState.sizeBeforeInterruption = Element.SizeUnspecified
- sceneState.sizeInterruptionDelta = IntSize.Zero
+ stateInContent.sizeBeforeInterruption = Element.SizeUnspecified
+ stateInContent.sizeInterruptionDelta = IntSize.Zero
return placeable
}
@@ -912,10 +924,10 @@
value = targetSize,
unspecifiedValue = Element.SizeUnspecified,
zeroValue = IntSize.Zero,
- getValueBeforeInterruption = { sceneState.sizeBeforeInterruption },
- setValueBeforeInterruption = { sceneState.sizeBeforeInterruption = it },
- getInterruptionDelta = { sceneState.sizeInterruptionDelta },
- setInterruptionDelta = { sceneState.sizeInterruptionDelta = it },
+ getValueBeforeInterruption = { stateInContent.sizeBeforeInterruption },
+ setValueBeforeInterruption = { stateInContent.sizeBeforeInterruption = it },
+ getInterruptionDelta = { stateInContent.sizeInterruptionDelta },
+ setInterruptionDelta = { stateInContent.sizeInterruptionDelta = it },
diff = { a, b -> IntSize(a.width - b.width, a.height - b.height) },
add = { a, b, bProgress ->
IntSize(
@@ -939,15 +951,15 @@
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
transition: TransitionState.Transition?,
- sceneState: Element.SceneState,
+ stateInContent: Element.State,
): Scale {
val scale =
computeValue(
layoutImpl,
- sceneState,
+ stateInContent,
element,
transition,
- sceneValue = { Scale.Default },
+ contentValue = { Scale.Default },
transformation = { it.drawScale },
currentValue = { Scale.Default },
isSpecified = { true },
@@ -965,16 +977,18 @@
value = scale,
unspecifiedValue = Scale.Unspecified,
zeroValue = Scale.Zero,
- getValueBeforeInterruption = { sceneState.scaleBeforeInterruption },
- setValueBeforeInterruption = { sceneState.scaleBeforeInterruption = it },
- getInterruptionDelta = { sceneState.scaleInterruptionDelta },
+ getValueBeforeInterruption = { stateInContent.scaleBeforeInterruption },
+ setValueBeforeInterruption = { stateInContent.scaleBeforeInterruption = it },
+ getInterruptionDelta = { stateInContent.scaleInterruptionDelta },
setInterruptionDelta = { delta ->
setPlacementInterruptionDelta(
element = element,
- sceneState = sceneState,
+ stateInContent = stateInContent,
transition = transition,
delta = delta,
- setter = { sceneState, delta -> sceneState.scaleInterruptionDelta = delta },
+ setter = { stateInContent, delta ->
+ stateInContent.scaleInterruptionDelta = delta
+ },
)
},
diff = { a, b ->
@@ -1003,7 +1017,7 @@
}
)
- sceneState.lastScale = interruptedScale
+ stateInContent.lastScale = interruptedScale
return interruptedScale
}
@@ -1015,11 +1029,11 @@
* Measurable.
*
* @param layoutImpl the [SceneTransitionLayoutImpl] associated to [element].
- * @param currentSceneState the scene state of the scene for which we are computing the value. Note
- * that during interruptions, this could be the state of a scene that is neither
+ * @param currentContentState the content state of the content for which we are computing the value.
+ * Note that during interruptions, this could be the state of a content that is neither
* [transition.toScene] nor [transition.fromScene].
* @param element the element being animated.
- * @param sceneValue the value being animated.
+ * @param contentValue the value being animated.
* @param transformation the transformation associated to the value being animated.
* @param currentValue the value that would be used if it is not transformed. Note that this is
* different than [idleValue] even if the value is not transformed directly because it could be
@@ -1030,10 +1044,10 @@
*/
private inline fun <T> computeValue(
layoutImpl: SceneTransitionLayoutImpl,
- currentSceneState: Element.SceneState,
+ currentContentState: Element.State,
element: Element,
transition: TransitionState.Transition?,
- sceneValue: (Element.SceneState) -> T,
+ contentValue: (Element.State) -> T,
transformation: (ElementTransformations) -> PropertyTransformation<T>?,
currentValue: () -> T,
isSpecified: (T) -> Boolean,
@@ -1050,16 +1064,16 @@
val fromScene = transition.fromScene
val toScene = transition.toScene
- val fromState = element.sceneStates[fromScene]
- val toState = element.sceneStates[toScene]
+ val fromState = element.stateByContent[fromScene]
+ val toState = element.stateByContent[toScene]
if (fromState == null && toState == null) {
// TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
// run anymore.
- return sceneValue(currentSceneState)
+ return contentValue(currentContentState)
}
- val currentScene = currentSceneState.scene
+ val currentScene = currentContentState.content
if (transition is TransitionState.HasOverscrollProperties) {
val overscroll = transition.currentOverscrollSpec
if (overscroll?.scene == currentScene) {
@@ -1067,7 +1081,7 @@
overscroll.transformationSpec.transformations(element.key, currentScene)
val propertySpec = transformation(elementSpec) ?: return currentValue()
val overscrollState = checkNotNull(if (currentScene == toScene) toState else fromState)
- val idleValue = sceneValue(overscrollState)
+ val idleValue = contentValue(overscrollState)
val targetValue =
propertySpec.transform(
layoutImpl,
@@ -1102,8 +1116,8 @@
// elements follow the finger direction.
val isSharedElement = fromState != null && toState != null
if (isSharedElement && isSharedElementEnabled(element.key, transition)) {
- val start = sceneValue(fromState!!)
- val end = sceneValue(toState!!)
+ val start = contentValue(fromState!!)
+ val end = contentValue(toState!!)
// TODO(b/316901148): Remove checks to isSpecified() once the lookahead pass runs for all
// nodes before the intermediate layout pass.
@@ -1117,7 +1131,7 @@
// Get the transformed value, i.e. the target value at the beginning (for entering elements) or
// end (for leaving elements) of the transition.
- val sceneState =
+ val contentState =
checkNotNull(
when {
isSharedElement && currentScene == fromScene -> fromState
@@ -1129,26 +1143,26 @@
// The scene for which we compute the transformation. Note that this is not necessarily
// [currentScene] because [currentScene] could be a different scene than the transition
// fromScene or toScene during interruptions.
- val scene = sceneState.scene
+ val content = contentState.content
val transformation =
- transformation(transition.transformationSpec.transformations(element.key, scene))
+ transformation(transition.transformationSpec.transformations(element.key, content))
val previewTransformation =
transition.previewTransformationSpec?.let {
- transformation(it.transformations(element.key, scene))
+ transformation(it.transformations(element.key, content))
}
if (previewTransformation != null) {
val isInPreviewStage = transition.isInPreviewStage
- val idleValue = sceneValue(sceneState)
- val isEntering = scene == toScene
+ val idleValue = contentValue(contentState)
+ val isEntering = content == toScene
val previewTargetValue =
previewTransformation.transform(
layoutImpl,
- scene,
+ content,
element,
- sceneState,
+ contentState,
transition,
idleValue,
)
@@ -1156,9 +1170,9 @@
val targetValueOrNull =
transformation?.transform(
layoutImpl,
- scene,
+ content,
element,
- sceneState,
+ contentState,
transition,
idleValue,
)
@@ -1226,13 +1240,13 @@
return currentValue()
}
- val idleValue = sceneValue(sceneState)
+ val idleValue = contentValue(contentState)
val targetValue =
transformation.transform(
layoutImpl,
- scene,
+ content,
element,
- sceneState,
+ contentState,
transition,
idleValue,
)
@@ -1248,7 +1262,7 @@
val rangeProgress = transformation.range?.progress(progress) ?: progress
// Interpolate between the value at rest and the value before entering/after leaving.
- val isEntering = scene == toScene
+ val isEntering = content == toScene
return if (isEntering) {
lerp(targetValue, idleValue, rangeProgress)
} else {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt
index 98dbb67..ca68c25 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt
@@ -18,20 +18,23 @@
/** An interface to match one or more elements. */
interface ElementMatcher {
- /** Whether the element with key [key] in scene [scene] matches this matcher. */
- fun matches(key: ElementKey, scene: SceneKey): Boolean
+ /** Whether the element with key [key] in scene [content] matches this matcher. */
+ fun matches(key: ElementKey, content: ContentKey): Boolean
}
/**
- * Returns an [ElementMatcher] that matches elements in [scene] also matching [this]
+ * Returns an [ElementMatcher] that matches elements in [content] also matching [this]
* [ElementMatcher].
*/
-fun ElementMatcher.inScene(scene: SceneKey): ElementMatcher {
+fun ElementMatcher.inContent(content: ContentKey): ElementMatcher {
val delegate = this
- val matcherScene = scene
+ val matcherScene = content
return object : ElementMatcher {
- override fun matches(key: ElementKey, scene: SceneKey): Boolean {
- return scene == matcherScene && delegate.matches(key, scene)
+ override fun matches(key: ElementKey, content: ContentKey): Boolean {
+ return content == matcherScene && delegate.matches(key, content)
}
}
}
+
+@Deprecated("Use inContent() instead", replaceWith = ReplaceWith("inContent(scene)"))
+fun ElementMatcher.inScene(scene: SceneKey) = inContent(scene)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
index 9770399..a9edf0a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
@@ -40,15 +40,20 @@
}
}
+/** The key for a content (scene or overlay). */
+sealed class ContentKey(debugName: String, identity: Any) : Key(debugName, identity) {
+ @VisibleForTesting
+ // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
+ // access internal members.
+ abstract val testTag: String
+}
+
/** Key for a scene. */
class SceneKey(
debugName: String,
identity: Any = Object(),
-) : Key(debugName, identity) {
- @VisibleForTesting
- // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
- // access internal members.
- val testTag: String = "scene:$debugName"
+) : ContentKey(debugName, identity) {
+ override val testTag: String = "scene:$debugName"
/** The unique [ElementKey] identifying this scene's root element. */
val rootElementKey = ElementKey(debugName, identity)
@@ -74,7 +79,7 @@
// access internal members.
val testTag: String = "element:$debugName"
- override fun matches(key: ElementKey, scene: SceneKey): Boolean {
+ override fun matches(key: ElementKey, content: ContentKey): Boolean {
return key == this
}
@@ -86,7 +91,7 @@
/** Matches any element whose [key identity][ElementKey.identity] matches [predicate]. */
fun withIdentity(predicate: (Any) -> Boolean): ElementMatcher {
return object : ElementMatcher {
- override fun matches(key: ElementKey, scene: SceneKey): Boolean {
+ override fun matches(key: ElementKey, content: ContentKey): Boolean {
return predicate(key.identity)
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
index 32eadde..e556f6f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
@@ -27,21 +27,22 @@
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastLastOrNull
+import com.android.compose.animation.scene.content.Content
@Composable
internal fun Element(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
+ sceneOrOverlay: Content,
key: ElementKey,
modifier: Modifier,
content: @Composable ElementScope<ElementContentScope>.() -> Unit,
) {
- Box(modifier.element(layoutImpl, scene, key)) {
- val sceneScope = scene.scope
+ Box(modifier.element(layoutImpl, sceneOrOverlay, key)) {
+ val contentScope = sceneOrOverlay.scope
val boxScope = this
val elementScope =
- remember(layoutImpl, key, scene, sceneScope, boxScope) {
- ElementScopeImpl(layoutImpl, key, scene, sceneScope, boxScope)
+ remember(layoutImpl, key, sceneOrOverlay, contentScope, boxScope) {
+ ElementScopeImpl(layoutImpl, key, sceneOrOverlay, contentScope, boxScope)
}
content(elementScope)
@@ -51,17 +52,17 @@
@Composable
internal fun MovableElement(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
+ sceneOrOverlay: Content,
key: ElementKey,
modifier: Modifier,
content: @Composable ElementScope<MovableElementContentScope>.() -> Unit,
) {
- Box(modifier.element(layoutImpl, scene, key)) {
- val sceneScope = scene.scope
+ Box(modifier.element(layoutImpl, sceneOrOverlay, key)) {
+ val contentScope = sceneOrOverlay.scope
val boxScope = this
val elementScope =
- remember(layoutImpl, key, scene, sceneScope, boxScope) {
- MovableElementScopeImpl(layoutImpl, key, scene, sceneScope, boxScope)
+ remember(layoutImpl, key, sceneOrOverlay, contentScope, boxScope) {
+ MovableElementScopeImpl(layoutImpl, key, sceneOrOverlay, contentScope, boxScope)
}
content(elementScope)
@@ -71,7 +72,7 @@
private abstract class BaseElementScope<ContentScope>(
private val layoutImpl: SceneTransitionLayoutImpl,
private val element: ElementKey,
- private val scene: Scene,
+ private val sceneOrOverlay: Content,
) : ElementScope<ContentScope> {
@Composable
override fun <T> animateElementValueAsState(
@@ -82,7 +83,7 @@
): AnimatedState<T> {
return animateSharedValueAsState(
layoutImpl,
- scene.key,
+ sceneOrOverlay.key,
element,
key,
value,
@@ -95,12 +96,12 @@
private class ElementScopeImpl(
layoutImpl: SceneTransitionLayoutImpl,
element: ElementKey,
- scene: Scene,
- private val sceneScope: SceneScope,
+ content: Content,
+ private val delegateContentScope: ContentScope,
private val boxScope: BoxScope,
-) : BaseElementScope<ElementContentScope>(layoutImpl, element, scene) {
+) : BaseElementScope<ElementContentScope>(layoutImpl, element, content) {
private val contentScope =
- object : ElementContentScope, SceneScope by sceneScope, BoxScope by boxScope {}
+ object : ElementContentScope, ContentScope by delegateContentScope, BoxScope by boxScope {}
@Composable
override fun content(content: @Composable ElementContentScope.() -> Unit) {
@@ -111,12 +112,15 @@
private class MovableElementScopeImpl(
private val layoutImpl: SceneTransitionLayoutImpl,
private val element: ElementKey,
- private val scene: Scene,
- private val sceneScope: BaseSceneScope,
+ private val content: Content,
+ private val baseContentScope: BaseContentScope,
private val boxScope: BoxScope,
-) : BaseElementScope<MovableElementContentScope>(layoutImpl, element, scene) {
+) : BaseElementScope<MovableElementContentScope>(layoutImpl, element, content) {
private val contentScope =
- object : MovableElementContentScope, BaseSceneScope by sceneScope, BoxScope by boxScope {}
+ object :
+ MovableElementContentScope,
+ BaseContentScope by baseContentScope,
+ BoxScope by boxScope {}
@Composable
override fun content(content: @Composable MovableElementContentScope.() -> Unit) {
@@ -126,9 +130,10 @@
// during the transition.
// TODO(b/317026105): Use derivedStateOf only if the scene picker reads the progress in its
// logic.
+ val contentKey = this@MovableElementScopeImpl.content.key
val shouldComposeMovableElement by
- remember(layoutImpl, scene.key, element) {
- derivedStateOf { shouldComposeMovableElement(layoutImpl, scene.key, element) }
+ remember(layoutImpl, contentKey, element) {
+ derivedStateOf { shouldComposeMovableElement(layoutImpl, contentKey, element) }
}
if (shouldComposeMovableElement) {
@@ -152,7 +157,7 @@
val size =
placeholderContentSize(
layoutImpl,
- scene.key,
+ contentKey,
layoutImpl.elements.getValue(element),
)
layout(size.width, size.height) {}
@@ -163,7 +168,7 @@
private fun shouldComposeMovableElement(
layoutImpl: SceneTransitionLayoutImpl,
- scene: SceneKey,
+ content: ContentKey,
element: ElementKey,
): Boolean {
val transitions = layoutImpl.state.currentTransitions
@@ -171,7 +176,7 @@
// If we are idle, there is only one [scene] that is composed so we can compose our
// movable content here. We still check that [scene] is equal to the current idle scene, to
// make sure we only compose it there.
- return layoutImpl.state.transitionState.currentScene == scene
+ return layoutImpl.state.transitionState.currentScene == content
}
// The current transition for this element is the last transition in which either fromScene or
@@ -189,7 +194,7 @@
// Always compose movable elements in the scene picked by their scene picker.
return shouldPlaceOrComposeSharedElement(
layoutImpl,
- scene,
+ content,
element,
transition,
)
@@ -201,12 +206,12 @@
*/
private fun placeholderContentSize(
layoutImpl: SceneTransitionLayoutImpl,
- scene: SceneKey,
+ content: ContentKey,
element: Element,
): IntSize {
// If the content of the movable element was already composed in this scene before, use that
// target size.
- val targetValueInScene = element.sceneStates.getValue(scene).targetSize
+ val targetValueInScene = element.stateByContent.getValue(content).targetSize
if (targetValueInScene != Element.SizeUnspecified) {
return targetValueInScene
}
@@ -219,8 +224,9 @@
// doesn't change between scenes.
// TODO(b/317026105): Provide a way to give a hint size/content for cases where this is not
// true.
- val otherScene = if (transition.fromScene == scene) transition.toScene else transition.fromScene
- val targetValueInOtherScene = element.sceneStates[otherScene]?.targetSize
+ val otherScene =
+ if (transition.fromScene == content) transition.toScene else transition.fromScene
+ val targetValueInOtherScene = element.stateByContent[otherScene]?.targetSize
if (targetValueInOtherScene != null && targetValueInOtherScene != Element.SizeUnspecified) {
return targetValueInOtherScene
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 2fc4526..3401af8 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -34,7 +34,6 @@
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
-import com.android.compose.animation.scene.UserAction.Resolved
/**
* [SceneTransitionLayout] is a container that automatically animates its content whenever its state
@@ -85,7 +84,7 @@
fun scene(
key: SceneKey,
userActions: Map<UserAction, UserActionResult> = emptyMap(),
- content: @Composable SceneScope.() -> Unit,
+ content: @Composable ContentScope.() -> Unit,
)
}
@@ -118,25 +117,25 @@
@Stable
@ElementDsl
-interface BaseSceneScope : ElementStateScope {
- /** The key of this scene. */
- val sceneKey: SceneKey
+interface BaseContentScope : ElementStateScope {
+ /** The key of this content. */
+ val contentKey: ContentKey
- /** The state of the [SceneTransitionLayout] in which this scene is contained. */
+ /** The state of the [SceneTransitionLayout] in which this content is contained. */
val layoutState: SceneTransitionLayoutState
/**
* Tag an element identified by [key].
*
* Tagging an element will allow you to reference that element when defining transitions, so
- * that the element can be transformed and animated when the scene transitions in or out.
+ * that the element can be transformed and animated when the content transitions in or out.
*
- * Additionally, this [key] will be used to detect elements that are shared between scenes to
+ * Additionally, this [key] will be used to detect elements that are shared between contents to
* automatically interpolate their size and offset. If you need to animate shared element values
- * (i.e. values associated to this element that change depending on which scene it is composed
+ * (i.e. values associated to this element that change depending on which content it is composed
* in), use [Element] instead.
*
- * Note that shared elements tagged using this function will be duplicated in each scene they
+ * Note that shared elements tagged using this function will be duplicated in each content they
* are part of, so any **internal** state (e.g. state created using `remember {
* mutableStateOf(...) }`) will be lost. If you need to preserve internal state, you should use
* [MovableElement] instead.
@@ -150,7 +149,7 @@
* Create an element identified by [key].
*
* Similar to [element], this creates an element that will be automatically shared when present
- * in multiple scenes and that can be transformed during transitions, the same way that
+ * in multiple contents and that can be transformed during transitions, the same way that
* [element] does.
*
* The only difference with [element] is that the provided [ElementScope] allows you to
@@ -177,7 +176,7 @@
* Create a *movable* element identified by [key].
*
* Similar to [Element], this creates an element that will be automatically shared when present
- * in multiple scenes and that can be transformed during transitions, and you can also use the
+ * in multiple contents and that can be transformed during transitions, and you can also use the
* provided [ElementScope] to [animate element values][ElementScope.animateElementValueAsState].
*
* The important difference with [element] and [Element] is that this element
@@ -232,24 +231,26 @@
fun Modifier.noResizeDuringTransitions(): Modifier
}
+typealias SceneScope = ContentScope
+
@Stable
@ElementDsl
-interface SceneScope : BaseSceneScope {
+interface ContentScope : BaseContentScope {
/**
- * Animate some value at the scene level.
+ * Animate some value at the content level.
*
* @param value the value of this shared value in the current scene.
* @param key the key of this shared value.
* @param type the [SharedValueType] of this animated value.
* @param canOverflow whether this value can overflow past the values it is interpolated
* between, for instance because the transition is animated using a bouncy spring.
- * @see animateSceneIntAsState
- * @see animateSceneFloatAsState
- * @see animateSceneDpAsState
- * @see animateSceneColorAsState
+ * @see animateContentIntAsState
+ * @see animateContentFloatAsState
+ * @see animateContentDpAsState
+ * @see animateContentColorAsState
*/
@Composable
- fun <T> animateSceneValueAsState(
+ fun <T> animateContentValueAsState(
value: T,
key: ValueKey,
type: SharedValueType<T, *>,
@@ -259,7 +260,7 @@
/**
* The type of a shared value animated using [ElementScope.animateElementValueAsState] or
- * [SceneScope.animateSceneValueAsState].
+ * [ContentScope.animateContentValueAsState].
*/
@Stable
interface SharedValueType<T, Delta> {
@@ -321,8 +322,9 @@
* The exact same scope as [androidx.compose.foundation.layout.BoxScope].
*
* We can't reuse BoxScope directly because of the @LayoutScopeMarker annotation on it, which would
- * prevent us from calling Modifier.element() and other methods of [SceneScope] inside any Box {} in
- * the [content][ElementScope.content] of a [SceneScope.Element] or a [SceneScope.MovableElement].
+ * prevent us from calling Modifier.element() and other methods of [ContentScope] inside any Box {}
+ * in the [content][ElementScope.content] of a [ContentScope.Element] or a
+ * [ContentScope.MovableElement].
*/
@Stable
@ElementDsl
@@ -335,16 +337,16 @@
}
/** The scope for "normal" (not movable) elements. */
-@Stable @ElementDsl interface ElementContentScope : SceneScope, ElementBoxScope
+@Stable @ElementDsl interface ElementContentScope : ContentScope, ElementBoxScope
/**
* The scope for the content of movable elements.
*
- * Note that it extends [BaseSceneScope] and not [SceneScope] because movable elements should not
- * call [SceneScope.animateSceneValueAsState], given that their content is not composed in all
- * scenes.
+ * Note that it extends [BaseContentScope] and not [ContentScope] because movable elements should
+ * not call [ContentScope.animateContentValueAsState], given that their content is not composed in
+ * all scenes.
*/
-@Stable @ElementDsl interface MovableElementContentScope : BaseSceneScope, ElementBoxScope
+@Stable @ElementDsl interface MovableElementContentScope : BaseContentScope, ElementBoxScope
/** An action performed by the user. */
sealed class UserAction {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 32db0b7..062d553 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -36,6 +36,8 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachReversed
+import com.android.compose.animation.scene.content.Content
+import com.android.compose.animation.scene.content.Scene
import com.android.compose.ui.util.lerp
import kotlinx.coroutines.CoroutineScope
@@ -84,7 +86,7 @@
/**
* The different values of a shared value keyed by a a [ValueKey] and the different elements and
- * scenes it is associated to.
+ * contents it is associated to.
*/
private var _sharedValues: MutableMap<ValueKey, MutableMap<ElementKey?, SharedValue<*, *>>>? =
null
@@ -149,6 +151,12 @@
return scenes[key] ?: error("Scene $key is not configured")
}
+ internal fun content(key: ContentKey): Content {
+ return when (key) {
+ is SceneKey -> scene(key)
+ }
+ }
+
internal fun updateScenes(
builder: SceneTransitionLayoutScope.() -> Unit,
layoutDirection: LayoutDirection,
@@ -164,7 +172,7 @@
override fun scene(
key: SceneKey,
userActions: Map<UserAction, UserActionResult>,
- content: @Composable SceneScope.() -> Unit,
+ content: @Composable ContentScope.() -> Unit,
) {
scenesToRemove.remove(key)
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 06b093d..cfa4c70 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
@@ -302,18 +302,18 @@
override val distance: UserActionDistance?,
override val transformations: List<Transformation>,
) : TransformationSpec {
- private val cache = mutableMapOf<ElementKey, MutableMap<SceneKey, ElementTransformations>>()
+ private val cache = mutableMapOf<ElementKey, MutableMap<ContentKey, ElementTransformations>>()
- internal fun transformations(element: ElementKey, scene: SceneKey): ElementTransformations {
+ internal fun transformations(element: ElementKey, content: ContentKey): ElementTransformations {
return cache
.getOrPut(element) { mutableMapOf() }
- .getOrPut(scene) { computeTransformations(element, scene) }
+ .getOrPut(content) { computeTransformations(element, content) }
}
/** Filter [transformations] to compute the [ElementTransformations] of [element]. */
private fun computeTransformations(
element: ElementKey,
- scene: SceneKey,
+ content: ContentKey,
): ElementTransformations {
var shared: SharedElementTransformation? = null
var offset: PropertyTransformation<Offset>? = null
@@ -351,7 +351,7 @@
}
transformations.fastForEach { transformation ->
- if (!transformation.matcher.matches(element, scene)) {
+ if (!transformation.matcher.matches(element, content)) {
return@fastForEach
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index a2118b2..f062146 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -31,6 +31,7 @@
import androidx.compose.ui.node.TraversableNode
import androidx.compose.ui.node.findNearestAncestor
import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.content.Scene
/**
* Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state.
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 3a87d41..06be86d 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
@@ -239,10 +239,11 @@
* should not be drawn or composed in neither [transition.fromScene] nor [transition.toScene],
* return `null`.
*
- * Important: For [MovableElements][SceneScope.MovableElement], this scene picker will *always*
- * be used during transitions to decide whether we should compose that element in a given scene
- * or not. Therefore, you should make sure that the returned [SceneKey] contains the movable
- * element, otherwise that element will not be composed in any scene during the transition.
+ * Important: For [MovableElements][ContentScope.MovableElement], this scene picker will
+ * *always* be used during transitions to decide whether we should compose that element in a
+ * given scene or not. Therefore, you should make sure that the returned [SceneKey] contains the
+ * movable element, otherwise that element will not be composed in any scene during the
+ * transition.
*/
fun sceneDuringTransition(
element: ElementKey,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
index b7abb33..0f66804 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
@@ -23,13 +23,13 @@
private val layoutImpl: SceneTransitionLayoutImpl,
) : ElementStateScope {
override fun ElementKey.targetSize(scene: SceneKey): IntSize? {
- return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetSize.takeIf {
+ return layoutImpl.elements[this]?.stateByContent?.get(scene)?.targetSize.takeIf {
it != Element.SizeUnspecified
}
}
override fun ElementKey.targetOffset(scene: SceneKey): Offset? {
- return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetOffset.takeIf {
+ return layoutImpl.elements[this]?.stateByContent?.get(scene)?.targetOffset.takeIf {
it != Offset.Unspecified
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
similarity index 63%
rename from packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
rename to packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
index a49f1af..492d211 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.compose.animation.scene
+package com.android.compose.animation.scene.content
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
@@ -29,24 +29,44 @@
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.zIndex
+import com.android.compose.animation.scene.AnimatedState
+import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.ContentScope
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementContentScope
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementScope
+import com.android.compose.animation.scene.ElementStateScope
+import com.android.compose.animation.scene.MovableElement
+import com.android.compose.animation.scene.MovableElementContentScope
+import com.android.compose.animation.scene.NestedScrollBehavior
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.SceneTransitionLayoutState
+import com.android.compose.animation.scene.SharedValueType
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.ValueKey
+import com.android.compose.animation.scene.animateSharedValueAsState
+import com.android.compose.animation.scene.element
import com.android.compose.animation.scene.modifiers.noResizeDuringTransitions
+import com.android.compose.animation.scene.nestedScrollToScene
-/** A scene in a [SceneTransitionLayout]. */
+/** A content defined in a [SceneTransitionLayout], i.e. a scene or an overlay. */
@Stable
-internal class Scene(
- val key: SceneKey,
- layoutImpl: SceneTransitionLayoutImpl,
- content: @Composable SceneScope.() -> Unit,
+internal sealed class Content(
+ open val key: ContentKey,
+ val layoutImpl: SceneTransitionLayoutImpl,
+ content: @Composable ContentScope.() -> Unit,
actions: Map<UserAction.Resolved, UserActionResult>,
zIndex: Float,
) {
- internal val scope = SceneScopeImpl(layoutImpl, this)
+ internal val scope = ContentScopeImpl(layoutImpl, content = this)
var content by mutableStateOf(content)
- private var _userActions by mutableStateOf(checkValid(actions))
var zIndex by mutableFloatStateOf(zIndex)
var targetSize by mutableStateOf(IntSize.Zero)
+ private var _userActions by mutableStateOf(checkValid(actions))
var userActions
get() = _userActions
set(value) {
@@ -59,8 +79,8 @@
userActions.forEach { (action, result) ->
if (key == result.toScene) {
error(
- "Transition to the same scene is not supported. Scene $key, action $action," +
- " result $result"
+ "Transition to the same content (scene/overlay) is not supported. Content " +
+ "$key, action $action, result $result"
)
}
}
@@ -73,7 +93,7 @@
modifier
.zIndex(zIndex)
.approachLayout(
- isMeasurementApproachInProgress = { scope.layoutState.isTransitioning() }
+ isMeasurementApproachInProgress = { layoutImpl.state.isTransitioning() }
) { measurable, constraints ->
targetSize = lookaheadSize
val placeable = measurable.measure(constraints)
@@ -84,21 +104,19 @@
scope.content()
}
}
-
- override fun toString(): String {
- return "Scene(key=$key)"
- }
}
-internal class SceneScopeImpl(
+internal class ContentScopeImpl(
private val layoutImpl: SceneTransitionLayoutImpl,
- private val scene: Scene,
-) : SceneScope, ElementStateScope by layoutImpl.elementStateScope {
- override val sceneKey: SceneKey = scene.key
+ private val content: Content,
+) : ContentScope, ElementStateScope by layoutImpl.elementStateScope {
+ override val contentKey: ContentKey
+ get() = content.key
+
override val layoutState: SceneTransitionLayoutState = layoutImpl.state
override fun Modifier.element(key: ElementKey): Modifier {
- return element(layoutImpl, scene, key)
+ return element(layoutImpl, content, key)
}
@Composable
@@ -107,7 +125,7 @@
modifier: Modifier,
content: @Composable (ElementScope<ElementContentScope>.() -> Unit)
) {
- Element(layoutImpl, scene, key, modifier, content)
+ Element(layoutImpl, this@ContentScopeImpl.content, key, modifier, content)
}
@Composable
@@ -116,19 +134,19 @@
modifier: Modifier,
content: @Composable (ElementScope<MovableElementContentScope>.() -> Unit)
) {
- MovableElement(layoutImpl, scene, key, modifier, content)
+ MovableElement(layoutImpl, this@ContentScopeImpl.content, key, modifier, content)
}
@Composable
- override fun <T> animateSceneValueAsState(
+ override fun <T> animateContentValueAsState(
value: T,
key: ValueKey,
type: SharedValueType<T, *>,
- canOverflow: Boolean
+ canOverflow: Boolean,
): AnimatedState<T> {
return animateSharedValueAsState(
layoutImpl = layoutImpl,
- scene = scene.key,
+ content = content.key,
element = null,
key = key,
value = value,
@@ -141,27 +159,29 @@
leftBehavior: NestedScrollBehavior,
rightBehavior: NestedScrollBehavior,
isExternalOverscrollGesture: () -> Boolean,
- ): Modifier =
- nestedScrollToScene(
+ ): Modifier {
+ return nestedScrollToScene(
layoutImpl = layoutImpl,
orientation = Orientation.Horizontal,
topOrLeftBehavior = leftBehavior,
bottomOrRightBehavior = rightBehavior,
isExternalOverscrollGesture = isExternalOverscrollGesture,
)
+ }
override fun Modifier.verticalNestedScrollToScene(
topBehavior: NestedScrollBehavior,
bottomBehavior: NestedScrollBehavior,
isExternalOverscrollGesture: () -> Boolean,
- ): Modifier =
- nestedScrollToScene(
+ ): Modifier {
+ return nestedScrollToScene(
layoutImpl = layoutImpl,
orientation = Orientation.Vertical,
topOrLeftBehavior = topBehavior,
bottomOrRightBehavior = bottomBehavior,
isExternalOverscrollGesture = isExternalOverscrollGesture,
)
+ }
override fun Modifier.noResizeDuringTransitions(): Modifier {
return noResizeDuringTransitions(layoutState = layoutImpl.state)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt
new file mode 100644
index 0000000..4a7a94d
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.compose.animation.scene.content
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import com.android.compose.animation.scene.ContentScope
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+
+/** A scene defined in a [SceneTransitionLayout]. */
+@Stable
+internal class Scene(
+ override val key: SceneKey,
+ layoutImpl: SceneTransitionLayoutImpl,
+ content: @Composable ContentScope.() -> Unit,
+ actions: Map<UserAction.Resolved, UserActionResult>,
+ zIndex: Float,
+) : Content(key, layoutImpl, content, actions, zIndex) {
+ override fun toString(): String {
+ return "Scene(key=$key)"
+ }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
index 73ee451..65d4d2d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene.transformation
import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.ElementMatcher
@@ -33,15 +34,15 @@
) : PropertyTransformation<IntSize> {
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: SceneKey,
+ content: ContentKey,
element: Element,
- sceneState: Element.SceneState,
+ sceneState: Element.State,
transition: TransitionState.Transition,
value: IntSize,
): IntSize {
fun anchorSizeIn(scene: SceneKey): IntSize {
val size =
- layoutImpl.elements[anchor]?.sceneStates?.get(scene)?.targetSize?.takeIf {
+ layoutImpl.elements[anchor]?.stateByContent?.get(scene)?.targetSize?.takeIf {
it != Element.SizeUnspecified
}
?: throwMissingAnchorException(
@@ -59,7 +60,7 @@
// This simple implementation assumes that the size of [element] is the same as the size of
// the [anchor] in [scene], so simply transform to the size of the anchor in the other
// scene.
- return if (scene == transition.fromScene) {
+ return if (content == transition.fromScene) {
anchorSizeIn(transition.toScene)
} else {
anchorSizeIn(transition.fromScene)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
index 70dca4c..8d7e1c9 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
@@ -18,6 +18,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isSpecified
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.ElementMatcher
@@ -32,9 +33,9 @@
) : PropertyTransformation<Offset> {
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: SceneKey,
+ content: ContentKey,
element: Element,
- sceneState: Element.SceneState,
+ sceneState: Element.State,
transition: TransitionState.Transition,
value: Offset,
): Offset {
@@ -48,7 +49,7 @@
val anchor = layoutImpl.elements[anchor] ?: throwException(scene = null)
fun anchorOffsetIn(scene: SceneKey): Offset? {
- return anchor.sceneStates[scene]?.targetOffset?.takeIf { it.isSpecified }
+ return anchor.stateByContent[scene]?.targetOffset?.takeIf { it.isSpecified }
}
// [element] will move the same amount as [anchor] does.
@@ -60,7 +61,7 @@
anchorOffsetIn(transition.toScene) ?: throwException(transition.toScene)
val offset = anchorToOffset - anchorFromOffset
- return if (scene == transition.toScene) {
+ return if (content == transition.toScene) {
Offset(
value.x - offset.x,
value.y - offset.y,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
index 98c2dd3..f010c3b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
@@ -17,10 +17,10 @@
package com.android.compose.animation.scene.transformation
import androidx.compose.ui.geometry.Offset
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.Scale
-import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransitionState
@@ -37,9 +37,9 @@
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: SceneKey,
+ content: ContentKey,
element: Element,
- sceneState: Element.SceneState,
+ sceneState: Element.State,
transition: TransitionState.Transition,
value: Scale,
): Scale {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
index 7daefd0..dfce997 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -17,10 +17,10 @@
package com.android.compose.animation.scene.transformation
import androidx.compose.ui.geometry.Offset
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransitionState
@@ -32,13 +32,13 @@
) : PropertyTransformation<Offset> {
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: SceneKey,
+ content: ContentKey,
element: Element,
- sceneState: Element.SceneState,
+ sceneState: Element.State,
transition: TransitionState.Transition,
value: Offset
): Offset {
- val sceneSize = layoutImpl.scene(scene).targetSize
+ val sceneSize = layoutImpl.content(content).targetSize
val elementSize = sceneState.targetSize
if (elementSize == Element.SizeUnspecified) {
return value
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
index ada814e..c1bb017 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
@@ -16,9 +16,9 @@
package com.android.compose.animation.scene.transformation
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransitionState
@@ -28,9 +28,9 @@
) : PropertyTransformation<Float> {
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: SceneKey,
+ content: ContentKey,
element: Element,
- sceneState: Element.SceneState,
+ sceneState: Element.State,
transition: TransitionState.Transition,
value: Float
): Float {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
index dca8f85..5adbf7e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
@@ -17,9 +17,9 @@
package com.android.compose.animation.scene.transformation
import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransitionState
import kotlin.math.roundToInt
@@ -35,9 +35,9 @@
) : PropertyTransformation<IntSize> {
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: SceneKey,
+ content: ContentKey,
element: Element,
- sceneState: Element.SceneState,
+ sceneState: Element.State,
transition: TransitionState.Transition,
value: IntSize,
): IntSize {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
index 7be9ce1..24b7194 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -19,9 +19,9 @@
import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastCoerceAtMost
import androidx.compose.ui.util.fastCoerceIn
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransitionState
@@ -61,9 +61,9 @@
// to these internal classes.
fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: SceneKey,
+ content: ContentKey,
element: Element,
- sceneState: Element.SceneState,
+ sceneState: Element.State,
transition: TransitionState.Transition,
value: T,
): T
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
index f066511..123756a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
@@ -19,10 +19,10 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.OverscrollScope
-import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransitionState
@@ -33,9 +33,9 @@
) : PropertyTransformation<Offset> {
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: SceneKey,
+ content: ContentKey,
element: Element,
- sceneState: Element.SceneState,
+ sceneState: Element.State,
transition: TransitionState.Transition,
value: Offset,
): Offset {
@@ -55,9 +55,9 @@
) : PropertyTransformation<Offset> {
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: SceneKey,
+ content: ContentKey,
element: Element,
- sceneState: Element.SceneState,
+ sceneState: Element.State,
transition: TransitionState.Transition,
value: Offset,
): Offset {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt
index 8e35988..ae3169b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt
@@ -57,7 +57,7 @@
minHeight() < currentHeight && currentHeight < maxHeight()
},
canScrollOnFling = true,
- onStart = { /* do nothing */},
+ onStart = { /* do nothing */ },
onScroll = { offsetAvailable ->
val currentHeight = height()
val amountConsumed =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
index ac11d30..228f7ba 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
@@ -38,7 +38,7 @@
private val canStartPreScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean,
private val canStartPostScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean,
private val canStartPostFling: (velocityAvailable: Velocity) -> Boolean,
- private val canContinueScroll: () -> Boolean,
+ private val canContinueScroll: (source: NestedScrollSource) -> Boolean,
private val canScrollOnFling: Boolean,
private val onStart: (offsetAvailable: Offset) -> Unit,
private val onScroll: (offsetAvailable: Offset) -> Offset,
@@ -61,7 +61,7 @@
if (
isPriorityMode ||
- (source == NestedScrollSource.Fling && !canScrollOnFling) ||
+ (source == NestedScrollSource.SideEffect && !canScrollOnFling) ||
!canStartPostScroll(available, offsetBeforeStart)
) {
// The priority mode cannot start so we won't consume the available offset.
@@ -73,7 +73,7 @@
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (!isPriorityMode) {
- if (source != NestedScrollSource.Fling || canScrollOnFling) {
+ if (source == NestedScrollSource.UserInput || canScrollOnFling) {
if (canStartPreScroll(available, offsetScrolledBeforePriorityMode)) {
return onPriorityStart(available)
}
@@ -84,7 +84,7 @@
return Offset.Zero
}
- if (!canContinueScroll()) {
+ if (!canContinueScroll(source)) {
// Step 3a: We have lost priority and we no longer need to intercept scroll events.
onPriorityStop(velocity = Velocity.Zero)
@@ -170,7 +170,7 @@
canStartPreScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
canStartPostScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
canStartPostFling: (velocityAvailable: Float) -> Boolean,
- canContinueScroll: () -> Boolean,
+ canContinueScroll: (source: NestedScrollSource) -> Boolean,
canScrollOnFling: Boolean,
onStart: (offsetAvailable: Float) -> Unit,
onScroll: (offsetAvailable: Float) -> Float,
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
index a7889e2..0f33303 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
@@ -67,7 +67,7 @@
}
@Composable
- private fun SceneScope.Foo(
+ private fun ContentScope.Foo(
targetValues: Values,
onCurrentValueChanged: (Values) -> Unit,
) {
@@ -87,7 +87,7 @@
}
@Composable
- private fun SceneScope.MovableFoo(
+ private fun ContentScope.MovableFoo(
targetValues: Values,
onCurrentValueChanged: (Values) -> Unit,
) {
@@ -105,14 +105,14 @@
}
@Composable
- private fun SceneScope.SceneValues(
+ private fun ContentScope.SceneValues(
targetValues: Values,
onCurrentValueChanged: (Values) -> Unit,
) {
- val int by animateSceneIntAsState(targetValues.int, key = TestValues.Value1)
- val float by animateSceneFloatAsState(targetValues.float, key = TestValues.Value2)
- val dp by animateSceneDpAsState(targetValues.dp, key = TestValues.Value3)
- val color by animateSceneColorAsState(targetValues.color, key = TestValues.Value4)
+ val int by animateContentIntAsState(targetValues.int, key = TestValues.Value1)
+ val float by animateContentFloatAsState(targetValues.float, key = TestValues.Value2)
+ val dp by animateContentDpAsState(targetValues.dp, key = TestValues.Value3)
+ val color by animateContentColorAsState(targetValues.color, key = TestValues.Value4)
LaunchedEffect(Unit) {
snapshotFlow { Values(int, float, dp, color) }.collect(onCurrentValueChanged)
@@ -292,7 +292,7 @@
fun readingAnimatedStateValueDuringCompositionThrows() {
assertThrows(IllegalStateException::class.java) {
rule.testTransition(
- fromSceneContent = { animateSceneIntAsState(0, TestValues.Value1).value },
+ fromSceneContent = { animateContentIntAsState(0, TestValues.Value1).value },
toSceneContent = {},
transition = {},
) {}
@@ -302,21 +302,21 @@
@Test
fun readingAnimatedStateValueDuringCompositionIsStillPossible() {
@Composable
- fun SceneScope.SceneValuesDuringComposition(
+ fun ContentScope.SceneValuesDuringComposition(
targetValues: Values,
onCurrentValueChanged: (Values) -> Unit,
) {
val int by
- animateSceneIntAsState(targetValues.int, key = TestValues.Value1)
+ animateContentIntAsState(targetValues.int, key = TestValues.Value1)
.unsafeCompositionState(targetValues.int)
val float by
- animateSceneFloatAsState(targetValues.float, key = TestValues.Value2)
+ animateContentFloatAsState(targetValues.float, key = TestValues.Value2)
.unsafeCompositionState(targetValues.float)
val dp by
- animateSceneDpAsState(targetValues.dp, key = TestValues.Value3)
+ animateContentDpAsState(targetValues.dp, key = TestValues.Value3)
.unsafeCompositionState(targetValues.dp)
val color by
- animateSceneColorAsState(targetValues.color, key = TestValues.Value4)
+ animateContentColorAsState(targetValues.color, key = TestValues.Value4)
.unsafeCompositionState(targetValues.color)
val values = Values(int, float, dp, color)
@@ -397,14 +397,14 @@
val foo = ValueKey("foo")
val bar = ValueKey("bar")
- val lastValues = mutableMapOf<ValueKey, MutableMap<SceneKey, Float>>()
+ val lastValues = mutableMapOf<ValueKey, MutableMap<ContentKey, Float>>()
@Composable
- fun SceneScope.animateFloat(value: Float, key: ValueKey) {
- val animatedValue = animateSceneFloatAsState(value, key)
+ fun ContentScope.animateFloat(value: Float, key: ValueKey) {
+ val animatedValue = animateContentFloatAsState(value, key)
LaunchedEffect(animatedValue) {
snapshotFlow { animatedValue.value }
- .collect { lastValues.getOrPut(key) { mutableMapOf() }[sceneKey] = it }
+ .collect { lastValues.getOrPut(key) { mutableMapOf() }[contentKey] = it }
}
}
@@ -458,13 +458,13 @@
}
val key = ValueKey("foo")
- val lastValues = mutableMapOf<SceneKey, Float>()
+ val lastValues = mutableMapOf<ContentKey, Float>()
@Composable
- fun SceneScope.animateFloat(value: Float, key: ValueKey) {
- val animatedValue = animateSceneFloatAsState(value, key)
+ fun ContentScope.animateFloat(value: Float, key: ValueKey) {
+ val animatedValue = animateContentFloatAsState(value, key)
LaunchedEffect(animatedValue) {
- snapshotFlow { animatedValue.value }.collect { lastValues[sceneKey] = it }
+ snapshotFlow { animatedValue.value }.collect { lastValues[contentKey] = it }
}
}
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 1d9e9b7..329257e 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
@@ -86,7 +86,7 @@
@get:Rule val rule = createComposeRule()
@Composable
- private fun SceneScope.Element(
+ private fun ContentScope.Element(
key: ElementKey,
size: Dp,
offset: Dp,
@@ -380,7 +380,7 @@
assertThat(layoutImpl.elements.keys).containsExactly(key)
val element = layoutImpl.elements.getValue(key)
- assertThat(element.sceneStates.keys).containsExactly(SceneB)
+ assertThat(element.stateByContent.keys).containsExactly(SceneB)
// Scene C, state 0: the same element is reused.
rule.runOnUiThread { state.setTargetScene(SceneC, coroutineScope) }
@@ -389,13 +389,13 @@
assertThat(layoutImpl.elements.keys).containsExactly(key)
assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
- assertThat(element.sceneStates.keys).containsExactly(SceneC)
+ assertThat(element.stateByContent.keys).containsExactly(SceneC)
// Scene C, state 1: the element is removed from the map.
sceneCState = 1
rule.waitForIdle()
- assertThat(element.sceneStates).isEmpty()
+ assertThat(element.stateByContent).isEmpty()
assertThat(layoutImpl.elements).isEmpty()
}
@@ -405,7 +405,7 @@
assertThrows(IllegalStateException::class.java) {
rule.setContent {
- TestSceneScope {
+ TestContentScope {
Column {
Box(Modifier.element(key))
Box(Modifier.element(key))
@@ -421,7 +421,7 @@
assertThrows(IllegalStateException::class.java) {
rule.setContent {
- TestSceneScope {
+ TestContentScope {
Column {
val childModifier = Modifier.element(key)
Box(childModifier)
@@ -439,7 +439,7 @@
assertThrows(IllegalStateException::class.java) {
var nElements by mutableStateOf(1)
rule.setContent {
- TestSceneScope {
+ TestContentScope {
Column {
val childModifier = Modifier.element(key)
repeat(nElements) { Box(childModifier) }
@@ -457,7 +457,7 @@
assertThrows(IllegalStateException::class.java) {
var key by mutableStateOf(TestElements.Foo)
rule.setContent {
- TestSceneScope {
+ TestContentScope {
Column {
Box(Modifier.element(key))
Box(Modifier.element(TestElements.Bar))
@@ -491,7 +491,7 @@
// There is only Foo in the elements map.
assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo)
val fooElement = layoutImpl.elements.getValue(TestElements.Foo)
- assertThat(fooElement.sceneStates.keys).containsExactly(SceneA)
+ assertThat(fooElement.stateByContent.keys).containsExactly(SceneA)
key = TestElements.Bar
@@ -499,8 +499,8 @@
rule.waitForIdle()
assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Bar)
val barElement = layoutImpl.elements.getValue(TestElements.Bar)
- assertThat(barElement.sceneStates.keys).containsExactly(SceneA)
- assertThat(fooElement.sceneStates).isEmpty()
+ assertThat(barElement.stateByContent.keys).containsExactly(SceneA)
+ assertThat(fooElement.stateByContent).isEmpty()
}
@Test
@@ -553,7 +553,7 @@
// There is only Foo in the elements map.
assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo)
val element = layoutImpl.elements.getValue(TestElements.Foo)
- val sceneValues = element.sceneStates
+ val sceneValues = element.stateByContent
assertThat(sceneValues.keys).containsExactly(SceneA)
// Get the ElementModifier node that should be reused later on when coming back to this
@@ -576,7 +576,7 @@
assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo)
val newElement = layoutImpl.elements.getValue(TestElements.Foo)
- val newSceneValues = newElement.sceneStates
+ val newSceneValues = newElement.stateByContent
assertThat(newElement).isNotEqualTo(element)
assertThat(newSceneValues).isNotEqualTo(sceneValues)
assertThat(newSceneValues.keys).containsExactly(SceneA)
@@ -677,7 +677,7 @@
modifier = Modifier.size(layoutWidth, layoutHeight)
) {
scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
- animateSceneFloatAsState(
+ animateContentFloatAsState(
value = animatedFloatRange.start,
key = TestValues.Value1,
false
@@ -686,7 +686,7 @@
}
scene(SceneB) {
val animatedFloat by
- animateSceneFloatAsState(
+ animateContentFloatAsState(
value = animatedFloatRange.endInclusive,
key = TestValues.Value1,
canOverflow = false
@@ -1215,15 +1215,15 @@
}
val layoutSize = DpSize(200.dp, 100.dp)
- val lastValues = mutableMapOf<SceneKey, Float>()
+ val lastValues = mutableMapOf<ContentKey, Float>()
@Composable
- fun SceneScope.Foo(size: Dp, value: Float, modifier: Modifier = Modifier) {
- val sceneKey = this.sceneKey
+ fun ContentScope.Foo(size: Dp, value: Float, modifier: Modifier = Modifier) {
+ val contentKey = this.contentKey
Element(TestElements.Foo, modifier.size(size)) {
val animatedValue = animateElementFloatAsState(value, TestValues.Value1)
LaunchedEffect(animatedValue) {
- snapshotFlow { animatedValue.value }.collect { lastValues[sceneKey] = it }
+ snapshotFlow { animatedValue.value }.collect { lastValues[contentKey] = it }
}
}
}
@@ -1388,8 +1388,8 @@
// The interruption values should be unspecified and deltas should be set to zero.
val foo = layoutImpl.elements.getValue(TestElements.Foo)
- assertThat(foo.sceneStates.keys).containsExactly(SceneC)
- val stateInC = foo.sceneStates.getValue(SceneC)
+ assertThat(foo.stateByContent.keys).containsExactly(SceneC)
+ val stateInC = foo.stateByContent.getValue(SceneC)
assertThat(stateInC.offsetBeforeInterruption).isEqualTo(Offset.Unspecified)
assertThat(stateInC.sizeBeforeInterruption).isEqualTo(Element.SizeUnspecified)
assertThat(stateInC.scaleBeforeInterruption).isEqualTo(Scale.Unspecified)
@@ -1423,7 +1423,7 @@
}
@Composable
- fun SceneScope.Foo(modifier: Modifier = Modifier) {
+ fun ContentScope.Foo(modifier: Modifier = Modifier) {
Box(modifier.element(TestElements.Foo).size(fooSize))
}
@@ -1542,8 +1542,8 @@
assertThat(layoutImpl.elements).containsKey(TestElements.Foo)
val foo = layoutImpl.elements.getValue(TestElements.Foo)
- assertThat(foo.sceneStates).containsKey(SceneB)
- val bState = foo.sceneStates.getValue(SceneB)
+ assertThat(foo.stateByContent).containsKey(SceneB)
+ val bState = foo.stateByContent.getValue(SceneB)
assertThat(bState.targetSize).isNotEqualTo(Element.SizeUnspecified)
assertThat(bState.targetOffset).isNotEqualTo(Offset.Unspecified)
@@ -1583,9 +1583,9 @@
rule.waitForIdle()
val foo = checkNotNull(layoutImpl.elements[TestElements.Foo])
- assertThat(foo.sceneStates[SceneA]).isNull()
+ assertThat(foo.stateByContent[SceneA]).isNull()
- val fooInB = foo.sceneStates[SceneB]
+ val fooInB = foo.stateByContent[SceneB]
assertThat(fooInB).isNotNull()
assertThat(fooInB!!.lastAlpha).isEqualTo(0.5f)
@@ -1599,7 +1599,7 @@
state.startTransition(transition(from = SceneB, to = SceneC, progress = { 0.3f }))
}
rule.waitForIdle()
- val fooInC = foo.sceneStates[SceneC]
+ val fooInC = foo.stateByContent[SceneC]
assertThat(fooInC).isNotNull()
assertThat(fooInC!!.lastAlpha).isEqualTo(1f)
assertThat(fooInB.lastAlpha).isEqualTo(Element.AlphaUnspecified)
@@ -1645,7 +1645,7 @@
rule.waitForIdle()
// Alpha of Foo should be 0f at interruption progress 100%.
- val fooInB = layoutImpl.elements.getValue(TestElements.Foo).sceneStates.getValue(SceneB)
+ val fooInB = layoutImpl.elements.getValue(TestElements.Foo).stateByContent.getValue(SceneB)
assertThat(fooInB.lastAlpha).isEqualTo(0f)
// Alpha of Foo should be 0.6f at interruption progress 0%.
@@ -1673,7 +1673,7 @@
}
@Composable
- fun SceneScope.Foo() {
+ fun ContentScope.Foo() {
Box(Modifier.element(TestElements.Foo).size(10.dp))
}
@@ -1724,7 +1724,7 @@
val fooInB = "fooInB"
@Composable
- fun SceneScope.MovableFoo(text: String, modifier: Modifier = Modifier) {
+ fun ContentScope.MovableFoo(text: String, modifier: Modifier = Modifier) {
MovableElement(TestElements.Foo, modifier) { content { Text(text) } }
}
@@ -1773,7 +1773,7 @@
}
@Composable
- fun SceneScope.SceneWithFoo(offset: DpOffset, modifier: Modifier = Modifier) {
+ fun ContentScope.SceneWithFoo(offset: DpOffset, modifier: Modifier = Modifier) {
Box(modifier.fillMaxSize()) {
Box(Modifier.offset(offset.x, offset.y).element(TestElements.Foo).size(100.dp))
}
@@ -1856,7 +1856,7 @@
val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
@Composable
- fun SceneScope.NestedFooBar() {
+ fun ContentScope.NestedFooBar() {
Box(Modifier.element(TestElements.Foo)) {
Box(Modifier.element(TestElements.Bar).size(10.dp))
}
@@ -1881,13 +1881,13 @@
val foo = layoutImpl.elements.getValue(TestElements.Foo)
val bar = layoutImpl.elements.getValue(TestElements.Bar)
- assertThat(foo.sceneStates).containsKey(SceneA)
- assertThat(bar.sceneStates).containsKey(SceneA)
- assertThat(foo.sceneStates).doesNotContainKey(SceneB)
- assertThat(bar.sceneStates).doesNotContainKey(SceneB)
+ assertThat(foo.stateByContent).containsKey(SceneA)
+ assertThat(bar.stateByContent).containsKey(SceneA)
+ assertThat(foo.stateByContent).doesNotContainKey(SceneB)
+ assertThat(bar.stateByContent).doesNotContainKey(SceneB)
- val fooInA = foo.sceneStates.getValue(SceneA)
- val barInA = bar.sceneStates.getValue(SceneA)
+ val fooInA = foo.stateByContent.getValue(SceneA)
+ val barInA = bar.stateByContent.getValue(SceneA)
assertThat(fooInA.lastOffset).isNotEqualTo(Offset.Unspecified)
assertThat(fooInA.lastAlpha).isNotEqualTo(Element.AlphaUnspecified)
assertThat(fooInA.lastScale).isNotEqualTo(Scale.Unspecified)
@@ -1903,11 +1903,11 @@
rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsDisplayed()
rule.onNode(isElement(TestElements.Bar, SceneB)).assertIsDisplayed()
- assertThat(foo.sceneStates).containsKey(SceneB)
- assertThat(bar.sceneStates).containsKey(SceneB)
+ assertThat(foo.stateByContent).containsKey(SceneB)
+ assertThat(bar.stateByContent).containsKey(SceneB)
- val fooInB = foo.sceneStates.getValue(SceneB)
- val barInB = bar.sceneStates.getValue(SceneB)
+ val fooInB = foo.stateByContent.getValue(SceneB)
+ val barInB = bar.stateByContent.getValue(SceneB)
assertThat(fooInA.lastOffset).isEqualTo(Offset.Unspecified)
assertThat(fooInA.lastAlpha).isEqualTo(Element.AlphaUnspecified)
assertThat(fooInA.lastScale).isEqualTo(Scale.Unspecified)
@@ -1938,8 +1938,8 @@
}
@Composable
- fun SceneScope.Foo() {
- Box(Modifier.testTag("fooParentIn${sceneKey.debugName}")) {
+ fun ContentScope.Foo() {
+ Box(Modifier.testTag("fooParentIn${contentKey.debugName}")) {
Box(Modifier.element(TestElements.Foo).size(20.dp))
}
}
@@ -1973,7 +1973,7 @@
val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
@Composable
- fun SceneScope.Foo(offset: Dp) {
+ fun ContentScope.Foo(offset: Dp) {
Box(Modifier.fillMaxSize()) {
Box(Modifier.offset(offset, offset).element(TestElements.Foo).size(20.dp))
}
@@ -2041,7 +2041,7 @@
}
@Composable
- fun SceneScope.Foo() {
+ fun ContentScope.Foo() {
Box(Modifier.element(TestElements.Foo).size(10.dp))
}
@@ -2062,7 +2062,11 @@
rule.waitForIdle()
assertThat(
- layoutImpl.elements.getValue(TestElements.Foo).sceneStates.getValue(SceneB).lastSize
+ layoutImpl.elements
+ .getValue(TestElements.Foo)
+ .stateByContent
+ .getValue(SceneB)
+ .lastSize
)
.isEqualTo(Element.SizeUnspecified)
}
@@ -2078,8 +2082,8 @@
// In A => B, Foo is not shared and first fades out from A then fades in
// B.
sharedElement(TestElements.Foo, enabled = false)
- fractionRange(end = 0.5f) { fade(TestElements.Foo.inScene(SceneA)) }
- fractionRange(start = 0.5f) { fade(TestElements.Foo.inScene(SceneB)) }
+ fractionRange(end = 0.5f) { fade(TestElements.Foo.inContent(SceneA)) }
+ fractionRange(start = 0.5f) { fade(TestElements.Foo.inContent(SceneB)) }
}
from(SceneB, to = SceneA) {
@@ -2091,7 +2095,7 @@
}
@Composable
- fun SceneScope.Foo(modifier: Modifier = Modifier) {
+ fun ContentScope.Foo(modifier: Modifier = Modifier) {
Box(modifier.element(TestElements.Foo).size(10.dp))
}
@@ -2149,7 +2153,7 @@
val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
@Composable
- fun SceneScope.Foo(modifier: Modifier = Modifier) {
+ fun ContentScope.Foo(modifier: Modifier = Modifier) {
Box(modifier.element(TestElements.Foo).size(10.dp))
}
@@ -2216,7 +2220,7 @@
// verify that preview transition for exiting elements is halfway played from
// current-scene-value -> preview-target-value
- val exiting1InB = layoutImpl.elements.getValue(exiting1).sceneStates.getValue(SceneB)
+ val exiting1InB = layoutImpl.elements.getValue(exiting1).stateByContent.getValue(SceneB)
// e.g. exiting1 is half scaled...
assertThat(exiting1InB.lastScale).isEqualTo(Scale(0.9f, 0.9f, Offset.Unspecified))
// ...and exiting2 is halfway translated from 0.dp to 20.dp...
@@ -2228,7 +2232,7 @@
// verify that preview transition for entering elements is halfway played from
// preview-target-value -> transition-target-value (or target-scene-value if no
// transition-target-value defined).
- val entering1InA = layoutImpl.elements.getValue(entering1).sceneStates.getValue(SceneA)
+ val entering1InA = layoutImpl.elements.getValue(entering1).stateByContent.getValue(SceneA)
// e.g. entering1 is half scaled between 0f and 0.5f -> 0.25f...
assertThat(entering1InA.lastScale).isEqualTo(Scale(0.25f, 0.25f, Offset.Unspecified))
// ...and entering2 is half way translated between 30.dp and 0.dp
@@ -2272,7 +2276,7 @@
// verify that exiting elements remain in the preview-end state if no further transition is
// defined for them in the second stage
- val exiting1InB = layoutImpl.elements.getValue(exiting1).sceneStates.getValue(SceneB)
+ val exiting1InB = layoutImpl.elements.getValue(exiting1).stateByContent.getValue(SceneB)
// i.e. exiting1 remains half scaled
assertThat(exiting1InB.lastScale).isEqualTo(Scale(0.9f, 0.9f, Offset.Unspecified))
// in case there is an additional transition defined for the second stage, verify that the
@@ -2286,7 +2290,7 @@
rule.onNode(isElement(exiting3)).assertSizeIsEqualTo(90.dp, 90.dp)
// verify that entering elements animate seamlessly to their target state
- val entering1InA = layoutImpl.elements.getValue(entering1).sceneStates.getValue(SceneA)
+ val entering1InA = layoutImpl.elements.getValue(entering1).stateByContent.getValue(SceneA)
// e.g. entering1, which was scaled from 0f to 0.25f during the preview phase, should now be
// half way scaled between 0.25f and its target-state of 1f -> 0.625f
assertThat(entering1InA.lastScale).isEqualTo(Scale(0.625f, 0.625f, Offset.Unspecified))
@@ -2318,7 +2322,7 @@
}
@Composable
- fun SceneScope.Foo(elementKey: ElementKey) {
+ fun ContentScope.Foo(elementKey: ElementKey) {
Box(Modifier.element(elementKey).size(100.dp))
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
index 9523896..821cc29 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
@@ -62,7 +62,7 @@
}
@Composable
- private fun SceneScope.MovableCounter(key: ElementKey, modifier: Modifier) {
+ private fun ContentScope.MovableCounter(key: ElementKey, modifier: Modifier) {
MovableElement(key, modifier) { content { Counter() } }
}
@@ -264,7 +264,7 @@
@Test
fun movableElementContentIsRecomposedIfContentParametersChange() {
@Composable
- fun SceneScope.MovableFoo(text: String, modifier: Modifier = Modifier) {
+ fun ContentScope.MovableFoo(text: String, modifier: Modifier = Modifier) {
MovableElement(TestElements.Foo, modifier) { content { Text(text) } }
}
@@ -298,7 +298,7 @@
@Test
fun elementScopeExtendsBoxScope() {
rule.setContent {
- TestSceneScope {
+ TestContentScope {
Element(TestElements.Foo, Modifier.size(200.dp)) {
content {
Box(Modifier.testTag("bottomEnd").align(Alignment.BottomEnd))
@@ -315,7 +315,7 @@
@Test
fun movableElementScopeExtendsBoxScope() {
rule.setContent {
- TestSceneScope {
+ TestContentScope {
MovableElement(TestElements.Foo, Modifier.size(200.dp)) {
content {
Box(Modifier.testTag("bottomEnd").align(Alignment.BottomEnd))
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
index 311a580..9ebc426 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
@@ -48,7 +48,7 @@
private val layoutHeight = 400.dp
private fun setup2ScenesAndScrollTouchSlop(
- modifierSceneA: @Composable SceneScope.() -> Modifier = { Modifier },
+ modifierSceneA: @Composable ContentScope.() -> Modifier = { Modifier },
): MutableSceneTransitionLayoutState {
val state =
rule.runOnUiThread {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 1ec1079..32f3bac 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -129,7 +129,7 @@
}
@Composable
- private fun SceneScope.SharedFoo(size: Dp, childOffset: Dp, modifier: Modifier = Modifier) {
+ private fun ContentScope.SharedFoo(size: Dp, childOffset: Dp, modifier: Modifier = Modifier) {
Element(TestElements.Foo, modifier.size(size).background(Color.Red)) {
// Offset the single child of Foo by some animated shared offset.
val offset by animateElementDpAsState(childOffset, TestValues.Value1)
@@ -479,14 +479,14 @@
fun sceneKeyInScope() {
val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
- var keyInA: SceneKey? = null
- var keyInB: SceneKey? = null
- var keyInC: SceneKey? = null
+ var keyInA: ContentKey? = null
+ var keyInB: ContentKey? = null
+ var keyInC: ContentKey? = null
rule.setContent {
SceneTransitionLayout(state) {
- scene(SceneA) { keyInA = sceneKey }
- scene(SceneB) { keyInB = sceneKey }
- scene(SceneC) { keyInC = sceneKey }
+ scene(SceneA) { keyInA = contentKey }
+ scene(SceneB) { keyInB = contentKey }
+ scene(SceneC) { keyInC = contentKey }
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
index 6233608..c9f71da 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
@@ -25,7 +25,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.TestElements
import com.android.compose.animation.scene.TransitionBuilder
import com.android.compose.animation.scene.TransitionRecordingSpec
@@ -108,8 +108,8 @@
}
private fun assertBarSizeMatchesGolden(
- fromSceneContent: @Composable SceneScope.() -> Unit,
- toSceneContent: @Composable SceneScope.() -> Unit,
+ fromSceneContent: @Composable ContentScope.() -> Unit,
+ toSceneContent: @Composable ContentScope.() -> Unit,
transition: TransitionBuilder.() -> Unit,
) {
val recordingSpec =
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
index 8001f41..00acb13 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
@@ -31,7 +31,7 @@
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TestElements
import com.android.compose.animation.scene.TestScenes
-import com.android.compose.animation.scene.inScene
+import com.android.compose.animation.scene.inContent
import com.android.compose.animation.scene.testTransition
import com.android.compose.test.assertSizeIsEqualTo
import org.junit.Rule
@@ -125,10 +125,10 @@
sharedElement(TestElements.Foo, enabled = false)
// In SceneA, Foo leaves to the left edge.
- translate(TestElements.Foo.inScene(TestScenes.SceneA), Edge.Left)
+ translate(TestElements.Foo.inContent(TestScenes.SceneA), Edge.Left)
// In SceneB, Foo comes from the bottom edge.
- translate(TestElements.Foo.inScene(TestScenes.SceneB), Edge.Bottom)
+ translate(TestElements.Foo.inContent(TestScenes.SceneB), Edge.Bottom)
},
) {
before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
similarity index 85%
rename from packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt
rename to packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
index fbd557f..00adefb 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
@@ -20,11 +20,13 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-/** `SceneScope` for tests, which allows a single scene to be drawn in a [SceneTransitionLayout]. */
+/**
+ * [ContentScope] for tests, which allows a single scene to be drawn in a [SceneTransitionLayout].
+ */
@Composable
-fun TestSceneScope(
+fun TestContentScope(
modifier: Modifier = Modifier,
- content: @Composable SceneScope.() -> Unit,
+ content: @Composable ContentScope.() -> Unit,
) {
val currentScene = remember { SceneKey("current") }
val state = remember { MutableSceneTransitionLayoutState(currentScene) }
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index a37d78e..7f26b98 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -18,9 +18,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.SemanticsNodeInteraction
@@ -87,8 +85,8 @@
* @sample com.android.compose.animation.scene.transformation.TranslateTest
*/
fun ComposeContentTestRule.testTransition(
- fromSceneContent: @Composable SceneScope.() -> Unit,
- toSceneContent: @Composable SceneScope.() -> Unit,
+ fromSceneContent: @Composable ContentScope.() -> Unit,
+ toSceneContent: @Composable ContentScope.() -> Unit,
transition: TransitionBuilder.() -> Unit,
layoutModifier: Modifier = Modifier,
fromScene: SceneKey = TestScenes.SceneA,
@@ -134,8 +132,8 @@
/** Records the transition between two scenes of [transitionLayout][SceneTransitionLayout]. */
fun MotionTestRule<ComposeToolkit>.recordTransition(
- fromSceneContent: @Composable SceneScope.() -> Unit,
- toSceneContent: @Composable SceneScope.() -> Unit,
+ fromSceneContent: @Composable ContentScope.() -> Unit,
+ toSceneContent: @Composable ContentScope.() -> Unit,
transition: TransitionBuilder.() -> Unit,
recordingSpec: TransitionRecordingSpec,
layoutModifier: Modifier = Modifier,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index 6ad4b31..3146318 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -675,6 +675,24 @@
assertThat(tiles!!.size).isEqualTo(3)
}
+ @Test
+ fun changeInPackagesTiles_doesntTriggerUserChange_logged() =
+ testScope.runTest(USER_INFO_0) {
+ val specs =
+ listOf(
+ TileSpec.create("a"),
+ )
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ runCurrent()
+ // Settled on the same list of tiles.
+ assertThat(underTest.currentTilesSpecs).isEqualTo(specs)
+
+ installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
+ runCurrent()
+
+ verify(logger, never()).logTileUserChanged(TileSpec.create("a"), 0)
+ }
+
private fun QSTile.State.fillIn(state: Int, label: CharSequence, secondaryLabel: CharSequence) {
this.state = state
this.label = label
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index d7c3527..ba37d58 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3681,7 +3681,10 @@
<!-- Touchpad back gesture action name in tutorial [CHAR LIMIT=NONE] -->
<string name="touchpad_back_gesture_action_title">Go back</string>
<!-- Touchpad back gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
- <string name="touchpad_back_gesture_guidance">To go back, swipe left or right using three fingers anywhere on the touchpad.</string>
+ <string name="touchpad_back_gesture_guidance">To go back, swipe left or right using three fingers anywhere on the touchpad.\n\nYou can also use the keyboard shortcut
+Action + ESC for this.</string>
+ <!-- Text shown to the user after they complete back gesture tutorial [CHAR LIMIT=NONE] -->
+ <string name="touchpad_back_gesture_finished">You completed the go back gesture.</string>
<string name="touchpad_back_gesture_animation_content_description">Touchpad showing three fingers moving right and left</string>
<string name="touchpad_back_gesture_screen_animation_content_description">Device screen showing animation for back gesture</string>
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 88601da..4286646 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -30,6 +30,7 @@
import com.android.systemui.dreams.DreamMonitor
import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable
import com.android.systemui.globalactions.GlobalActionsComponent
+import com.android.systemui.inputdevice.oobe.KeyboardTouchpadOobeTutorialCoreStartable
import com.android.systemui.keyboard.KeyboardUI
import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable
import com.android.systemui.keyguard.KeyguardViewConfigurator
@@ -257,6 +258,13 @@
@Binds
@IntoMap
+ @ClassKey(KeyboardTouchpadOobeTutorialCoreStartable::class)
+ abstract fun bindOobeSchedulerCoreStartable(
+ listener: KeyboardTouchpadOobeTutorialCoreStartable
+ ): CoreStartable
+
+ @Binds
+ @IntoMap
@ClassKey(PhysicalKeyboardCoreStartable::class)
abstract fun bindKeyboardCoreStartable(listener: PhysicalKeyboardCoreStartable): CoreStartable
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 15ddf5b..a448072 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -144,6 +144,7 @@
import com.android.systemui.statusbar.window.StatusBarWindowModule;
import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule;
import com.android.systemui.temporarydisplay.dagger.TemporaryDisplayModule;
+import com.android.systemui.touchpad.TouchpadModule;
import com.android.systemui.tuner.dagger.TunerModule;
import com.android.systemui.user.UserModule;
import com.android.systemui.user.domain.UserDomainLayerModule;
@@ -259,6 +260,7 @@
CommonSystemUIUnfoldModule.class,
TelephonyRepositoryModule.class,
TemporaryDisplayModule.class,
+ TouchpadModule.class,
TunerModule.class,
UserDomainLayerModule.class,
UserModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
index 3b161b6..5a008bd 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
@@ -45,7 +45,7 @@
data class DeviceAdded(val deviceId: Int) : DeviceChange
- data object DeviceRemoved : DeviceChange
+ data class DeviceRemoved(val deviceId: Int) : DeviceChange
data object FreshStart : DeviceChange
@@ -72,7 +72,7 @@
override fun onInputDeviceRemoved(deviceId: Int) {
connectedDevices = connectedDevices - deviceId
- sendWithLogging(connectedDevices to DeviceRemoved)
+ sendWithLogging(connectedDevices to DeviceRemoved(deviceId))
}
}
sendWithLogging(connectedDevices to FreshStart)
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt
new file mode 100644
index 0000000..dbfea76
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.systemui.inputdevice.oobe
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.inputdevice.oobe.domain.interactor.OobeTutorialSchedulerInteractor
+import com.android.systemui.shared.Flags.newTouchpadGesturesTutorial
+import dagger.Lazy
+import javax.inject.Inject
+
+/** A [CoreStartable] to launch a scheduler for keyboard and touchpad OOBE education */
+@SysUISingleton
+class KeyboardTouchpadOobeTutorialCoreStartable
+@Inject
+constructor(private val oobeTutorialSchedulerInteractor: Lazy<OobeTutorialSchedulerInteractor>) :
+ CoreStartable {
+ override fun start() {
+ if (newTouchpadGesturesTutorial()) {
+ oobeTutorialSchedulerInteractor.get().start()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt
new file mode 100644
index 0000000..0d69081
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.systemui.inputdevice.oobe.domain.interactor
+
+import android.content.Context
+import android.content.Intent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyboard.data.repository.KeyboardRepository
+import com.android.systemui.touchpad.data.repository.TouchpadRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/** When keyboards or touchpads are connected, schedule a tutorial after given time has elapsed */
+@SysUISingleton
+class OobeTutorialSchedulerInteractor
+@Inject
+constructor(
+ @Application private val context: Context,
+ @Application private val applicationScope: CoroutineScope,
+ keyboardRepository: KeyboardRepository,
+ touchpadRepository: TouchpadRepository
+) {
+ private val isAnyKeyboardConnected = keyboardRepository.isAnyKeyboardConnected
+ private val isAnyTouchpadConnected = touchpadRepository.isAnyTouchpadConnected
+
+ fun start() {
+ applicationScope.launch { isAnyKeyboardConnected.collect { startOobe() } }
+ applicationScope.launch { isAnyTouchpadConnected.collect { startOobe() } }
+ }
+
+ private fun startOobe() {
+ val intent = Intent(TUTORIAL_ACTION)
+ intent.addCategory(Intent.CATEGORY_DEFAULT)
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(intent)
+ }
+
+ companion object {
+ const val TAG = "OobeSchedulerInteractor"
+ const val TUTORIAL_ACTION = "com.android.systemui.action.TOUCHPAD_TUTORIAL"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
index 817849c..b654307 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
@@ -41,6 +41,7 @@
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
@@ -78,9 +79,15 @@
) : KeyboardRepository {
private val keyboardsChange: Flow<Pair<Collection<Int>, DeviceChange>> =
- inputDeviceRepository.deviceChange.map { (ids, change) ->
- ids.filter { id -> isPhysicalFullKeyboard(id) } to change
- }
+ inputDeviceRepository.deviceChange
+ .map { (ids, change) -> ids.filter { id -> isPhysicalFullKeyboard(id) } to change }
+ .filter { (_, change) ->
+ when (change) {
+ FreshStart -> true
+ is DeviceAdded -> isPhysicalFullKeyboard(change.deviceId)
+ is DeviceRemoved -> isPhysicalFullKeyboard(change.deviceId)
+ }
+ }
@FlowPreview
override val newlyConnectedKeyboard: Flow<Keyboard> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index af755d3..58719fe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -24,6 +24,9 @@
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@@ -77,11 +80,16 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
@@ -100,6 +108,7 @@
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
+import androidx.compose.ui.zIndex
import com.android.compose.ui.graphics.painter.rememberDrawablePainter
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
@@ -405,7 +414,7 @@
Row(Modifier.fillMaxWidth()) {
StartSidePanel(
onSearchQueryChanged = onSearchQueryChanged,
- modifier = Modifier.fillMaxWidth(fraction = 0.32f),
+ modifier = Modifier.width(200.dp),
categories = categories,
onKeyboardSettingsClicked = onKeyboardSettingsClicked,
selectedCategory = selectedCategoryType,
@@ -462,7 +471,18 @@
@Composable
private fun ShortcutView(modifier: Modifier, searchQuery: String, shortcut: Shortcut) {
- Row(modifier) {
+ val interactionSource = remember { MutableInteractionSource() }
+ val isFocused by interactionSource.collectIsFocusedAsState()
+ Row(
+ modifier
+ .focusable(interactionSource = interactionSource)
+ .outlineFocusModifier(
+ isFocused = isFocused,
+ focusColor = MaterialTheme.colorScheme.secondary,
+ padding = 8.dp,
+ cornerRadius = 16.dp
+ )
+ ) {
Row(
modifier = Modifier.width(128.dp).align(Alignment.CenterVertically),
horizontalArrangement = Arrangement.spacedBy(16.dp),
@@ -670,10 +690,23 @@
colors: NavigationDrawerItemColors =
NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent),
) {
+ val interactionSource = remember { MutableInteractionSource() }
+ val isFocused by interactionSource.collectIsFocusedAsState()
+
Surface(
selected = selected,
onClick = onClick,
- modifier = Modifier.semantics { role = Role.Tab }.heightIn(min = 64.dp).fillMaxWidth(),
+ modifier =
+ Modifier.semantics { role = Role.Tab }
+ .heightIn(min = 64.dp)
+ .fillMaxWidth()
+ .focusable(interactionSource = interactionSource)
+ .outlineFocusModifier(
+ isFocused = isFocused,
+ focusColor = MaterialTheme.colorScheme.secondary,
+ padding = 2.dp,
+ cornerRadius = 33.dp
+ ),
shape = RoundedCornerShape(28.dp),
color = colors.containerColor(selected).value,
) {
@@ -697,6 +730,39 @@
}
}
+private fun Modifier.outlineFocusModifier(
+ isFocused: Boolean,
+ focusColor: Color,
+ padding: Dp,
+ cornerRadius: Dp
+): Modifier {
+ if (isFocused) {
+ return this.drawWithContent {
+ val focusOutline =
+ Rect(Offset.Zero, size).let {
+ if (padding > 0.dp) {
+ it.inflate(padding.toPx())
+ } else {
+ it.deflate(padding.unaryMinus().toPx())
+ }
+ }
+ drawContent()
+ drawRoundRect(
+ color = focusColor,
+ style = Stroke(width = 3.dp.toPx()),
+ topLeft = focusOutline.topLeft,
+ size = focusOutline.size,
+ cornerRadius = CornerRadius(cornerRadius.toPx())
+ )
+ }
+ // Increasing Z-Index so focus outline is drawn on top of "selected" category
+ // background.
+ .zIndex(1f)
+ } else {
+ return this
+ }
+}
+
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun TitleBar() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
index e2d7851..04ea37e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -112,14 +112,18 @@
keyguardInteractor.isActiveDreamLockscreenHosted,
communalSceneInteractor.isIdleOnCommunal
)
- .filterRelevantKeyguardState()
- .collect {
- (isBouncerShowing, isAwake, isActiveDreamLockscreenHosted, isIdleOnCommunal)
- ->
+ .filterRelevantKeyguardStateAnd { (isBouncerShowing, _, _, _) ->
+ // TODO(b/307976454) - See if we need to listen for SHOW_WHEN_LOCKED
+ // activities showing up over the bouncer. Camera launch can't show up over
+ // bouncer since the first power press hides bouncer. Do occluding
+ // activities auto hide bouncer? Not sure.
+ !isBouncerShowing
+ }
+ .collect { (_, isAwake, isActiveDreamLockscreenHosted, isIdleOnCommunal) ->
if (
!maybeStartTransitionToOccludedOrInsecureCamera { state, reason ->
startTransitionTo(state, ownerReason = reason)
- } && !isBouncerShowing && isAwake && !isActiveDreamLockscreenHosted
+ } && isAwake && !isActiveDreamLockscreenHosted
) {
val toState =
if (isIdleOnCommunal) {
@@ -254,6 +258,6 @@
val TO_GONE_SHORT_DURATION = 200.milliseconds
val TO_LOCKSCREEN_DURATION = 450.milliseconds
val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION
- val TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD = 0.5f
+ val TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD = 0.1f
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
index 805dbb0..2ebd9e8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
@@ -30,6 +30,7 @@
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -52,11 +53,12 @@
* then we'll seed the repository with a transition from OFF -> GONE.
*/
@OptIn(ExperimentalCoroutinesApi::class)
- private val showLockscreenOnBoot =
+ private val showLockscreenOnBoot: Flow<Boolean> by lazy {
deviceProvisioningInteractor.isDeviceProvisioned.map { provisioned ->
(provisioned || deviceEntryInteractor.isAuthenticationRequired()) &&
deviceEntryInteractor.isLockscreenEnabled()
}
+ }
override fun start() {
scope.launch {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index 4bfefda..3fffeff 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -17,7 +17,7 @@
package com.android.systemui.keyguard.ui.viewmodel
import android.content.res.Resources
-import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.ContentKey
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.biometrics.AuthController
import com.android.systemui.dagger.SysUISingleton
@@ -90,12 +90,12 @@
/**
* Returns a flow that indicates whether lockscreen notifications should be rendered in the
- * given [sceneKey].
+ * given [contentKey].
*/
- fun areNotificationsVisible(sceneKey: SceneKey): Flow<Boolean> {
+ fun areNotificationsVisible(contentKey: ContentKey): Flow<Boolean> {
// `Scenes.NotificationsShade` renders its own separate notifications stack, so when it's
// open we avoid rendering the lockscreen notifications stack.
- if (sceneKey == Scenes.NotificationsShade) {
+ if (contentKey == Scenes.NotificationsShade) {
return flowOf(false)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
index 97b5e87..02379e6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -46,7 +46,7 @@
import com.android.systemui.retail.data.repository.RetailModeRepository
import com.android.systemui.settings.UserTracker
import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.kotlin.pairwise
+import com.android.systemui.util.kotlin.pairwiseBy
import dagger.Lazy
import java.io.PrintWriter
import javax.inject.Inject
@@ -63,7 +63,6 @@
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -169,17 +168,19 @@
private val userAndTiles =
currentUser
.flatMapLatest { userId ->
- tileSpecRepository.tilesSpecs(userId).map { UserAndTiles(userId, it) }
+ val currentTiles = tileSpecRepository.tilesSpecs(userId)
+ val installedComponents =
+ installedTilesComponentRepository.getInstalledTilesComponents(userId)
+ currentTiles.combine(installedComponents) { tiles, components ->
+ UserTilesAndComponents(userId, tiles, components)
+ }
}
.distinctUntilChanged()
- .pairwise(UserAndTiles(-1, emptyList()))
+ .pairwiseBy(UserTilesAndComponents(-1, emptyList(), emptySet())) { prev, new ->
+ DataWithUserChange(data = new, userChange = prev.userId != new.userId)
+ }
.flowOn(backgroundDispatcher)
- private val installedPackagesWithTiles =
- currentUser.flatMapLatest {
- installedTilesComponentRepository.getInstalledTilesComponents(it)
- }
-
private val minTiles: Int
get() =
if (retailModeRepository.inRetailMode) {
@@ -194,7 +195,6 @@
}
}
- @OptIn(ExperimentalCoroutinesApi::class)
private fun startTileCollection() {
scope.launch {
launch {
@@ -205,95 +205,82 @@
}
launch(backgroundDispatcher) {
- userAndTiles
- .combine(installedPackagesWithTiles) { usersAndTiles, packages ->
- Data(
- usersAndTiles.previousValue,
- usersAndTiles.newValue,
- packages,
- )
- }
- .collectLatest {
- val newTileList = it.newData.tiles
- val userChanged = it.oldData.userId != it.newData.userId
- val newUser = it.newData.userId
- val components = it.installedComponents
+ userAndTiles.collectLatest {
+ val newUser = it.userId
+ val newTileList = it.tiles
+ val components = it.installedComponents
+ val userChanged = it.userChange
- // Destroy all tiles that are not in the new set
- specsToTiles
- .filter {
- it.key !in newTileList && it.value is TileOrNotInstalled.Tile
- }
- .forEach { entry ->
- logger.logTileDestroyed(
- entry.key,
- if (userChanged) {
- QSPipelineLogger.TileDestroyedReason
- .TILE_NOT_PRESENT_IN_NEW_USER
- } else {
- QSPipelineLogger.TileDestroyedReason.TILE_REMOVED
- }
- )
- (entry.value as TileOrNotInstalled.Tile).tile.destroy()
- }
- // MutableMap will keep the insertion order
- val newTileMap = mutableMapOf<TileSpec, TileOrNotInstalled>()
-
- newTileList.forEach { tileSpec ->
- if (tileSpec !in newTileMap) {
- if (
- tileSpec is TileSpec.CustomTileSpec &&
- tileSpec.componentName !in components
- ) {
- newTileMap[tileSpec] = TileOrNotInstalled.NotInstalled
+ // Destroy all tiles that are not in the new set
+ specsToTiles
+ .filter { it.key !in newTileList && it.value is TileOrNotInstalled.Tile }
+ .forEach { entry ->
+ logger.logTileDestroyed(
+ entry.key,
+ if (userChanged) {
+ QSPipelineLogger.TileDestroyedReason
+ .TILE_NOT_PRESENT_IN_NEW_USER
} else {
- // Create tile here will never try to create a CustomTile that
- // is not installed
- val newTile =
- if (tileSpec in specsToTiles) {
- processExistingTile(
- tileSpec,
- specsToTiles.getValue(tileSpec),
- userChanged,
- newUser
- )
- ?: createTile(tileSpec)
- } else {
- createTile(tileSpec)
- }
- if (newTile != null) {
- newTileMap[tileSpec] = TileOrNotInstalled.Tile(newTile)
+ QSPipelineLogger.TileDestroyedReason.TILE_REMOVED
+ }
+ )
+ (entry.value as TileOrNotInstalled.Tile).tile.destroy()
+ }
+ // MutableMap will keep the insertion order
+ val newTileMap = mutableMapOf<TileSpec, TileOrNotInstalled>()
+
+ newTileList.forEach { tileSpec ->
+ if (tileSpec !in newTileMap) {
+ if (
+ tileSpec is TileSpec.CustomTileSpec &&
+ tileSpec.componentName !in components
+ ) {
+ newTileMap[tileSpec] = TileOrNotInstalled.NotInstalled
+ } else {
+ // Create tile here will never try to create a CustomTile that
+ // is not installed
+ val newTile =
+ if (tileSpec in specsToTiles) {
+ processExistingTile(
+ tileSpec,
+ specsToTiles.getValue(tileSpec),
+ userChanged,
+ newUser
+ ) ?: createTile(tileSpec)
+ } else {
+ createTile(tileSpec)
}
+ if (newTile != null) {
+ newTileMap[tileSpec] = TileOrNotInstalled.Tile(newTile)
}
}
}
-
- val resolvedSpecs = newTileMap.keys.toList()
- specsToTiles.clear()
- specsToTiles.putAll(newTileMap)
- val newResolvedTiles =
- newTileMap
- .filter { it.value is TileOrNotInstalled.Tile }
- .map {
- TileModel(it.key, (it.value as TileOrNotInstalled.Tile).tile)
- }
-
- _currentSpecsAndTiles.value = newResolvedTiles
- logger.logTilesNotInstalled(
- newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys,
- newUser
- )
- if (newResolvedTiles.size < minTiles) {
- // We ended up with not enough tiles (some may be not installed).
- // Prepend the default set of tiles
- launch { tileSpecRepository.prependDefault(currentUser.value) }
- } else if (resolvedSpecs != newTileList) {
- // There were some tiles that couldn't be created. Change the value in
- // the
- // repository
- launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) }
- }
}
+
+ val resolvedSpecs = newTileMap.keys.toList()
+ specsToTiles.clear()
+ specsToTiles.putAll(newTileMap)
+ val newResolvedTiles =
+ newTileMap
+ .filter { it.value is TileOrNotInstalled.Tile }
+ .map { TileModel(it.key, (it.value as TileOrNotInstalled.Tile).tile) }
+
+ _currentSpecsAndTiles.value = newResolvedTiles
+ logger.logTilesNotInstalled(
+ newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys,
+ newUser
+ )
+ if (newResolvedTiles.size < minTiles) {
+ // We ended up with not enough tiles (some may be not installed).
+ // Prepend the default set of tiles
+ launch { tileSpecRepository.prependDefault(currentUser.value) }
+ } else if (resolvedSpecs != newTileList) {
+ // There were some tiles that couldn't be created. Change the value in
+ // the
+ // repository
+ launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) }
+ }
+ }
}
}
}
@@ -362,8 +349,7 @@
newQSTileFactory.get().createTile(spec.spec)
} else {
null
- }
- ?: tileFactory.createTile(spec.spec)
+ } ?: tileFactory.createTile(spec.spec)
}
if (tile == null) {
logger.logTileNotFoundInFactory(spec)
@@ -436,15 +422,25 @@
@JvmInline value class Tile(val tile: QSTile) : TileOrNotInstalled
}
-
- private data class UserAndTiles(
- val userId: Int,
- val tiles: List<TileSpec>,
- )
-
- private data class Data(
- val oldData: UserAndTiles,
- val newData: UserAndTiles,
- val installedComponents: Set<ComponentName>,
- )
}
+
+private data class UserTilesAndComponents(
+ val userId: Int,
+ val tiles: List<TileSpec>,
+ val installedComponents: Set<ComponentName>
+)
+
+private data class DataWithUserChange(
+ val userId: Int,
+ val tiles: List<TileSpec>,
+ val installedComponents: Set<ComponentName>,
+ val userChange: Boolean,
+)
+
+private fun DataWithUserChange(data: UserTilesAndComponents, userChange: Boolean) =
+ DataWithUserChange(
+ data.userId,
+ data.tiles,
+ data.installedComponents,
+ userChange,
+ )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
index d9546ec..1750347 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
@@ -106,7 +106,8 @@
@Override
@MainThread
public void onManagedProfileRemoved() {
- mHost.removeTile(getTileSpec());
+ // No OP as this may race with the user change in CurrentTilesInteractor.
+ // If the tile needs to be removed, AutoAdd (or AutoTileManager) will take care of that.
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
index e9e9d8b..cdcefdb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
@@ -22,12 +22,15 @@
import com.android.internal.logging.InstanceId
import com.android.systemui.qs.pipeline.shared.TileSpec
-data class QSTileConfig(
+data class QSTileConfig
+@JvmOverloads
+constructor(
val tileSpec: TileSpec,
val uiConfig: QSTileUIConfig,
val instanceId: InstanceId,
val metricsSpec: String = tileSpec.spec,
val policy: QSTilePolicy = QSTilePolicy.NoRestrictions,
+ val autoRemoveOnUnavailable: Boolean = true,
)
/**
@@ -38,6 +41,7 @@
val iconRes: Int
@DrawableRes get
+
val labelRes: Int
@StringRes get
@@ -48,6 +52,7 @@
data object Empty : QSTileUIConfig {
override val iconRes: Int
get() = Resources.ID_NULL
+
override val labelRes: Int
get() = Resources.ID_NULL
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 2cdcc24..c6f9ae8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -70,7 +70,7 @@
applicationScope.launch {
launch {
qsTileViewModel.isAvailable.collectIndexed { index, isAvailable ->
- if (!isAvailable) {
+ if (!isAvailable && qsTileViewModel.config.autoRemoveOnUnavailable) {
qsHost.removeTile(tileSpec)
}
// qsTileViewModel.isAvailable flow often starts with isAvailable == true.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
index ac2a0d8..1e0e597a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
@@ -20,10 +20,14 @@
import android.os.UserHandle
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.server.notification.Flags.screenshareNotificationHiding
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
import com.android.systemui.statusbar.notification.DynamicPrivacyController
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry
@@ -32,27 +36,33 @@
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import dagger.Binds
import dagger.Module
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
@Module(includes = [PrivateSensitiveContentCoordinatorModule::class])
interface SensitiveContentCoordinatorModule
@Module
interface PrivateSensitiveContentCoordinatorModule {
- @Binds
- fun bindCoordinator(impl: SensitiveContentCoordinatorImpl): SensitiveContentCoordinator
+ @Binds fun bindCoordinator(impl: SensitiveContentCoordinatorImpl): SensitiveContentCoordinator
}
/** Coordinates re-inflation and post-processing of sensitive notification content. */
interface SensitiveContentCoordinator : Coordinator
@CoordinatorScope
-class SensitiveContentCoordinatorImpl @Inject constructor(
+class SensitiveContentCoordinatorImpl
+@Inject
+constructor(
private val dynamicPrivacyController: DynamicPrivacyController,
private val lockscreenUserManager: NotificationLockscreenUserManager,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@@ -61,45 +71,85 @@
private val selectedUserInteractor: SelectedUserInteractor,
private val sensitiveNotificationProtectionController:
SensitiveNotificationProtectionController,
-) : Invalidator("SensitiveContentInvalidator"),
- SensitiveContentCoordinator,
- DynamicPrivacyController.Listener,
- OnBeforeRenderListListener {
- private val onSensitiveStateChanged = Runnable() {
- invalidateList("onSensitiveStateChanged")
- }
+ private val deviceEntryInteractor: DeviceEntryInteractor,
+ private val sceneInteractor: SceneInteractor,
+ @Application private val scope: CoroutineScope,
+) :
+ Invalidator("SensitiveContentInvalidator"),
+ SensitiveContentCoordinator,
+ DynamicPrivacyController.Listener,
+ OnBeforeRenderListListener {
+ private var inTransitionFromLockedToGone = false
- private val screenshareSecretFilter = object : NotifFilter("ScreenshareSecretFilter") {
- val NotificationEntry.isSecret
- get() = channel?.lockscreenVisibility == Notification.VISIBILITY_SECRET ||
- sbn.notification?.visibility == Notification.VISIBILITY_SECRET
- override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean {
- return screenshareNotificationHiding() &&
- sensitiveNotificationProtectionController.isSensitiveStateActive &&
- entry.isSecret
+ private val onSensitiveStateChanged = Runnable() { invalidateList("onSensitiveStateChanged") }
+
+ private val screenshareSecretFilter =
+ object : NotifFilter("ScreenshareSecretFilter") {
+ val NotificationEntry.isSecret
+ get() =
+ channel?.lockscreenVisibility == Notification.VISIBILITY_SECRET ||
+ sbn.notification?.visibility == Notification.VISIBILITY_SECRET
+
+ override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean {
+ return screenshareNotificationHiding() &&
+ sensitiveNotificationProtectionController.isSensitiveStateActive &&
+ entry.isSecret
+ }
}
- }
override fun attach(pipeline: NotifPipeline) {
dynamicPrivacyController.addListener(this)
if (screenshareNotificationHiding()) {
- sensitiveNotificationProtectionController
- .registerSensitiveStateListener(onSensitiveStateChanged)
+ sensitiveNotificationProtectionController.registerSensitiveStateListener(
+ onSensitiveStateChanged
+ )
}
pipeline.addOnBeforeRenderListListener(this)
pipeline.addPreRenderInvalidator(this)
if (screenshareNotificationHiding()) {
pipeline.addFinalizeFilter(screenshareSecretFilter)
}
+
+ if (SceneContainerFlag.isEnabled) {
+ scope.launch {
+ sceneInteractor.transitionState
+ .mapNotNull {
+ val transitioningToGone = it.isTransitioning(to = Scenes.Gone)
+ val deviceEntered = deviceEntryInteractor.isDeviceEntered.value
+ when {
+ transitioningToGone && !deviceEntered -> true
+ !transitioningToGone -> false
+ else -> null
+ }
+ }
+ .distinctUntilChanged()
+ .collect {
+ inTransitionFromLockedToGone = it
+ invalidateList("inTransitionFromLockedToGoneChanged")
+ }
+ }
+ }
}
override fun onDynamicPrivacyChanged(): Unit = invalidateList("onDynamicPrivacyChanged")
+ private val isKeyguardGoingAway: Boolean
+ get() {
+ if (SceneContainerFlag.isEnabled) {
+ return inTransitionFromLockedToGone
+ } else {
+ return keyguardStateController.isKeyguardGoingAway
+ }
+ }
+
override fun onBeforeRenderList(entries: List<ListEntry>) {
- if (keyguardStateController.isKeyguardGoingAway ||
+ if (
+ isKeyguardGoingAway ||
statusBarStateController.state == StatusBarState.KEYGUARD &&
- keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(
- selectedUserInteractor.getSelectedUserId())) {
+ keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(
+ selectedUserInteractor.getSelectedUserId()
+ )
+ ) {
// don't update yet if:
// - the keyguard is currently going away
// - LS is about to be dismissed by a biometric that bypasses LS (avoid notif flash)
@@ -109,35 +159,40 @@
return
}
- val isSensitiveContentProtectionActive = screenshareNotificationHiding() &&
- sensitiveNotificationProtectionController.isSensitiveStateActive
+ val isSensitiveContentProtectionActive =
+ screenshareNotificationHiding() &&
+ sensitiveNotificationProtectionController.isSensitiveStateActive
val currentUserId = lockscreenUserManager.currentUserId
val devicePublic = lockscreenUserManager.isLockscreenPublicMode(currentUserId)
- val deviceSensitive = (devicePublic &&
+ val deviceSensitive =
+ (devicePublic &&
!lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId)) ||
isSensitiveContentProtectionActive
val dynamicallyUnlocked = dynamicPrivacyController.isDynamicallyUnlocked
for (entry in extractAllRepresentativeEntries(entries).filter { it.rowExists() }) {
val notifUserId = entry.sbn.user.identifier
- val userLockscreen = devicePublic ||
- lockscreenUserManager.isLockscreenPublicMode(notifUserId)
- val userPublic = when {
- // if we're not on the lockscreen, we're definitely private
- !userLockscreen -> false
- // we are on the lockscreen, so unless we're dynamically unlocked, we're
- // definitely public
- !dynamicallyUnlocked -> true
- // we're dynamically unlocked, but check if the notification needs
- // a separate challenge if it's from a work profile
- else -> when (notifUserId) {
- currentUserId -> false
- UserHandle.USER_ALL -> false
- else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId)
+ val userLockscreen =
+ devicePublic || lockscreenUserManager.isLockscreenPublicMode(notifUserId)
+ val userPublic =
+ when {
+ // if we're not on the lockscreen, we're definitely private
+ !userLockscreen -> false
+ // we are on the lockscreen, so unless we're dynamically unlocked, we're
+ // definitely public
+ !dynamicallyUnlocked -> true
+ // we're dynamically unlocked, but check if the notification needs
+ // a separate challenge if it's from a work profile
+ else ->
+ when (notifUserId) {
+ currentUserId -> false
+ UserHandle.USER_ALL -> false
+ else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId)
+ }
}
- }
- val shouldProtectNotification = screenshareNotificationHiding() &&
- sensitiveNotificationProtectionController.shouldProtectNotification(entry)
+ val shouldProtectNotification =
+ screenshareNotificationHiding() &&
+ sensitiveNotificationProtectionController.shouldProtectNotification(entry)
val needsRedaction = lockscreenUserManager.needsRedaction(entry)
val isSensitive = userPublic && needsRedaction
@@ -149,9 +204,7 @@
}
}
-private fun extractAllRepresentativeEntries(
- entries: List<ListEntry>
-): Sequence<NotificationEntry> =
+private fun extractAllRepresentativeEntries(entries: List<ListEntry>): Sequence<NotificationEntry> =
entries.asSequence().flatMap(::extractAllRepresentativeEntries)
private fun extractAllRepresentativeEntries(listEntry: ListEntry): Sequence<NotificationEntry> =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
index 2a8db56..a6ca3ab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
@@ -98,7 +98,8 @@
globalSettings.registerContentObserverSync(
globalSettings.getUriFor(HEADS_UP_NOTIFICATIONS_ENABLED),
/* notifyForDescendants = */ true,
- observer)
+ observer
+ )
// QQQ: Do we need to register for SETTING_HEADS_UP_TICKER? It seems unused.
@@ -147,12 +148,12 @@
}
}
-class PeekDndSuppressor() :
+class PeekDndSuppressor :
VisualInterruptionFilter(types = setOf(PEEK), reason = "suppressed by DND") {
override fun shouldSuppress(entry: NotificationEntry) = entry.shouldSuppressPeek()
}
-class PeekNotImportantSuppressor() :
+class PeekNotImportantSuppressor :
VisualInterruptionFilter(types = setOf(PEEK), reason = "importance < HIGH") {
override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_HIGH
}
@@ -170,7 +171,10 @@
class PeekOldWhenSuppressor(private val systemClock: SystemClock) :
VisualInterruptionFilter(
- types = setOf(PEEK), reason = "has old `when`", uiEventId = HUN_SUPPRESSED_OLD_WHEN) {
+ types = setOf(PEEK),
+ reason = "has old `when`",
+ uiEventId = HUN_SUPPRESSED_OLD_WHEN
+ ) {
private fun whenAge(entry: NotificationEntry) =
systemClock.currentTimeMillis() - entry.sbn.notification.getWhen()
@@ -190,47 +194,51 @@
}
}
-class PulseEffectSuppressor() :
+class PulseEffectSuppressor :
VisualInterruptionFilter(types = setOf(PULSE), reason = "suppressed by DND") {
override fun shouldSuppress(entry: NotificationEntry) = entry.shouldSuppressAmbient()
}
-class PulseLockscreenVisibilityPrivateSuppressor() :
+class PulseLockscreenVisibilityPrivateSuppressor :
VisualInterruptionFilter(
- types = setOf(PULSE), reason = "hidden by lockscreen visibility override") {
+ types = setOf(PULSE),
+ reason = "hidden by lockscreen visibility override"
+ ) {
override fun shouldSuppress(entry: NotificationEntry) =
entry.ranking.lockscreenVisibilityOverride == VISIBILITY_PRIVATE
}
-class PulseLowImportanceSuppressor() :
+class PulseLowImportanceSuppressor :
VisualInterruptionFilter(types = setOf(PULSE), reason = "importance < DEFAULT") {
override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_DEFAULT
}
-class HunGroupAlertBehaviorSuppressor() :
+class HunGroupAlertBehaviorSuppressor :
VisualInterruptionFilter(
- types = setOf(PEEK, PULSE), reason = "suppressive group alert behavior") {
+ types = setOf(PEEK, PULSE),
+ reason = "suppressive group alert behavior"
+ ) {
override fun shouldSuppress(entry: NotificationEntry) =
entry.sbn.let { it.isGroup && it.notification.suppressAlertingDueToGrouping() }
}
-class HunSilentNotificationSuppressor() :
+class HunSilentNotificationSuppressor :
VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "notification isSilent") {
override fun shouldSuppress(entry: NotificationEntry) =
entry.sbn.let { Flags.notificationSilentFlag() && it.notification.isSilent }
}
-class HunJustLaunchedFsiSuppressor() :
+class HunJustLaunchedFsiSuppressor :
VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "just launched FSI") {
override fun shouldSuppress(entry: NotificationEntry) = entry.hasJustLaunchedFullScreenIntent()
}
-class BubbleNotAllowedSuppressor() :
- VisualInterruptionFilter(types = setOf(BUBBLE), reason = "cannot bubble") {
+class BubbleNotAllowedSuppressor :
+ VisualInterruptionFilter(types = setOf(BUBBLE), reason = "cannot bubble", isSpammy = true) {
override fun shouldSuppress(entry: NotificationEntry) = !entry.canBubble()
}
-class BubbleNoMetadataSuppressor() :
+class BubbleNoMetadataSuppressor :
VisualInterruptionFilter(types = setOf(BUBBLE), reason = "has no or invalid bubble metadata") {
private fun isValidMetadata(metadata: BubbleMetadata?) =
@@ -253,6 +261,7 @@
/**
* Set with:
+ *
* adb shell setprop persist.force_show_avalanche_edu_once 1 && adb shell stop; adb shell start
*/
private const val FORCE_SHOW_AVALANCHE_EDU_ONCE = "persist.force_show_avalanche_edu_once"
@@ -368,7 +377,8 @@
val bundle = Bundle()
bundle.putString(
Notification.EXTRA_SUBSTITUTE_APP_NAME,
- context.getString(com.android.internal.R.string.android_system_label))
+ context.getString(com.android.internal.R.string.android_system_label)
+ )
val builder =
Notification.Builder(context, NotificationChannels.ALERTS)
@@ -390,8 +400,10 @@
}
private fun calculateState(entry: NotificationEntry): State {
- if (entry.ranking.isConversation &&
- entry.sbn.notification.getWhen() > avalancheProvider.startTime) {
+ if (
+ entry.ranking.isConversation &&
+ entry.sbn.notification.getWhen() > avalancheProvider.startTime
+ ) {
uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_NEW_CONVERSATION)
return State.ALLOW_CONVERSATION_AFTER_AVALANCHE
}
@@ -424,8 +436,10 @@
uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_COLORIZED)
return State.ALLOW_COLORIZED
}
- if (packageManager.checkPermission(RECEIVE_EMERGENCY_BROADCAST, entry.sbn.packageName) ==
- PERMISSION_GRANTED) {
+ if (
+ packageManager.checkPermission(RECEIVE_EMERGENCY_BROADCAST, entry.sbn.packageName) ==
+ PERMISSION_GRANTED
+ ) {
uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_EMERGENCY)
return State.ALLOW_EMERGENCY
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
index 1470b03..c204ea9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.interruption
+import android.util.Log
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel.DEBUG
import com.android.systemui.log.core.LogLevel.INFO
@@ -24,11 +25,15 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.FullScreenIntentDecision
import com.android.systemui.statusbar.notification.logKey
+import com.android.systemui.util.Compile
import javax.inject.Inject
class VisualInterruptionDecisionLogger
@Inject
constructor(@NotificationInterruptLog val buffer: LogBuffer) {
+
+ val spew: Boolean = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE)
+
fun logHeadsUpFeatureChanged(isEnabled: Boolean) {
buffer.log(
TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
index c0d27cb..8e8d9b6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
@@ -95,7 +95,8 @@
private constructor(
val decision: DecisionImpl,
override val uiEventId: UiEventEnum? = null,
- override val eventLogData: EventLogData? = null
+ override val eventLogData: EventLogData? = null,
+ val isSpammy: Boolean = false,
) : Loggable {
companion object {
val unsuppressed =
@@ -113,7 +114,8 @@
LoggableDecision(
DecisionImpl(shouldInterrupt = false, logReason = suppressor.reason),
uiEventId = suppressor.uiEventId,
- eventLogData = suppressor.eventLogData
+ eventLogData = suppressor.eventLogData,
+ isSpammy = suppressor.isSpammy,
)
}
}
@@ -185,8 +187,15 @@
if (NotificationAvalancheSuppression.isEnabled) {
addFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor,
- packageManager, uiEventLogger, context, notificationManager)
+ AvalancheSuppressor(
+ avalancheProvider,
+ systemClock,
+ settingsInteractor,
+ packageManager,
+ uiEventLogger,
+ context,
+ notificationManager
+ )
)
avalancheProvider.register()
}
@@ -280,7 +289,9 @@
entry: NotificationEntry,
loggableDecision: LoggableDecision
) {
- logger.logDecision(type.name, entry, loggableDecision.decision)
+ if (!loggableDecision.isSpammy || logger.spew) {
+ logger.logDecision(type.name, entry, loggableDecision.decision)
+ }
logEvents(entry, loggableDecision)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt
index ee79727..5fe75c0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt
@@ -59,6 +59,10 @@
/** Optional data to be logged in the EventLog when this suppresses an interruption. */
val eventLogData: EventLogData?
+ /** Whether the interruption is spammy and should be dropped under normal circumstances. */
+ val isSpammy: Boolean
+ get() = false
+
/**
* Called after the suppressor is added to the [VisualInterruptionDecisionProvider] but before
* any other methods are called on the suppressor.
@@ -76,7 +80,7 @@
constructor(
types: Set<VisualInterruptionType>,
reason: String
- ) : this(types, reason, /* uiEventId = */ null)
+ ) : this(types, reason, /* uiEventId= */ null)
/** @return true if these interruptions should be suppressed right now. */
abstract fun shouldSuppress(): Boolean
@@ -87,12 +91,13 @@
override val types: Set<VisualInterruptionType>,
override val reason: String,
override val uiEventId: UiEventEnum? = null,
- override val eventLogData: EventLogData? = null
+ override val eventLogData: EventLogData? = null,
+ override val isSpammy: Boolean = false,
) : VisualInterruptionSuppressor {
constructor(
types: Set<VisualInterruptionType>,
reason: String
- ) : this(types, reason, /* uiEventId = */ null)
+ ) : this(types, reason, /* uiEventId= */ null)
/**
* @param entry the notification to consider suppressing
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 461a38d..b6de78e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2995,7 +2995,7 @@
@Override
public void onFalse() {
// Hides quick settings, bouncer, and quick-quick settings.
- mStatusBarKeyguardViewManager.reset(true);
+ mStatusBarKeyguardViewManager.reset(true, /* isFalsingReset= */true);
}
};
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 41b69a7..88a2b23 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -708,7 +708,7 @@
* Shows the notification keyguard or the bouncer depending on
* {@link #needsFullscreenBouncer()}.
*/
- protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) {
+ protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
boolean isDozing = mDozing;
if (Flags.simPinRaceConditionOnRestart()) {
KeyguardState toState = mKeyguardTransitionInteractor.getTransitionState().getValue()
@@ -734,8 +734,12 @@
mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
}
}
- } else {
- Log.e(TAG, "Attempted to show the sim bouncer when it is already showing.");
+ } else if (!isFalsingReset) {
+ // Falsing resets can cause this to flicker, so don't reset in this case
+ Log.i(TAG, "Sim bouncer is already showing, issuing a refresh");
+ mPrimaryBouncerInteractor.hide();
+ mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+
}
} else {
mCentralSurfaces.showKeyguard();
@@ -957,6 +961,10 @@
@Override
public void reset(boolean hideBouncerWhenShowing) {
+ reset(hideBouncerWhenShowing, /* isFalsingReset= */false);
+ }
+
+ public void reset(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
if (mKeyguardStateController.isShowing() && !bouncerIsAnimatingAway()) {
final boolean isOccluded = mKeyguardStateController.isOccluded();
// Hide quick settings.
@@ -968,7 +976,7 @@
hideBouncer(false /* destroyView */);
}
} else {
- showBouncerOrKeyguard(hideBouncerWhenShowing);
+ showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset);
}
if (hideBouncerWhenShowing) {
hideAlternateBouncer(true);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index a7c5f78..03ec41d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -20,6 +20,7 @@
import android.telephony.TelephonyCallback
import android.telephony.TelephonyManager
import android.telephony.satellite.NtnSignalStrengthCallback
+import android.telephony.satellite.SatelliteCommunicationAllowedStateCallback
import android.telephony.satellite.SatelliteManager
import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS
import android.telephony.satellite.SatelliteModemStateCallback
@@ -37,7 +38,6 @@
import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteInputLog
import com.android.systemui.statusbar.pipeline.dagger.VerboseDeviceBasedSatelliteInputLog
import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository
-import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported
import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported
import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported
@@ -60,11 +60,9 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
@@ -122,15 +120,9 @@
}
/**
- * Basically your everyday run-of-the-mill system service listener, with three notable exceptions.
+ * Basically your everyday run-of-the-mill system service listener, with two notable exceptions.
*
- * First, there is an availability bit that we are tracking via [SatelliteManager]. See
- * [isSatelliteAllowedForCurrentLocation] for the implementation details. The thing to note about
- * this bit is that there is no callback that exists. Therefore we implement a simple polling
- * mechanism here. Since the underlying bit is location-dependent, we simply poll every hour (see
- * [POLLING_INTERVAL_MS]) and see what the current state is.
- *
- * Secondly, there are cases when simply requesting information from SatelliteManager can fail. See
+ * First, there are cases when simply requesting information from SatelliteManager can fail. See
* [SatelliteSupport] for details on how we track the state. What's worth noting here is that
* SUPPORTED is a stronger guarantee than [satelliteManager] being null. Therefore, the fundamental
* data flows here ([connectionState], [signalStrength],...) are wrapped in the convenience method
@@ -138,7 +130,7 @@
* [SupportedSatelliteManager], we can guarantee that the manager is non-null AND that it has told
* us that satellite is supported. Therefore, we don't expect exceptions to be thrown.
*
- * Lastly, this class is designed to wait a full minute of process uptime before making any requests
+ * Second, this class is designed to wait a full minute of process uptime before making any requests
* to the satellite manager. The hope is that by waiting we don't have to retry due to a modem that
* is still booting up or anything like that. We can tune or remove this behavior in the future if
* necessary.
@@ -158,8 +150,6 @@
private val satelliteManager: SatelliteManager?
- override val isSatelliteAllowedForCurrentLocation: MutableStateFlow<Boolean>
-
// Some calls into satellite manager will throw exceptions if it is not supported.
// This is never expected to change after boot, but may need to be retried in some cases
@get:VisibleForTesting
@@ -221,8 +211,6 @@
init {
satelliteManager = satelliteManagerOpt.getOrNull()
- isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
-
if (satelliteManager != null) {
// Outer scope launch allows us to delay until MIN_UPTIME
scope.launch {
@@ -233,10 +221,7 @@
{ "Checked for system support. support=$str1" },
)
- // Second, launch a job to poll for service availability based on location
- scope.launch { pollForAvailabilityBasedOnLocation() }
-
- // Third, register a listener to let us know if there are changes to support
+ // Second, register a listener to let us know if there are changes to support
scope.launch { listenForChangesToSatelliteSupport(satelliteManager) }
}
} else {
@@ -259,28 +244,43 @@
return sm.checkSatelliteSupported()
}
- /*
- * As there is no listener available for checking satellite allowed, we must poll the service.
- * Defaulting to polling at most once every 20m while active. Subsequent OOS events will restart
- * the job, so a flaky connection might cause more frequent checks.
- */
- private suspend fun pollForAvailabilityBasedOnLocation() {
+ override val isSatelliteAllowedForCurrentLocation =
satelliteSupport
.whenSupported(
- supported = ::isSatelliteAllowedHasListener,
+ supported = ::isSatelliteAvailableFlow,
orElse = flowOf(false),
retrySignal = telephonyProcessCrashedEvent,
)
- .collectLatest { hasSubscribers ->
- if (hasSubscribers) {
- while (true) {
- logBuffer.i { "requestIsCommunicationAllowedForCurrentLocation" }
- checkIsSatelliteAllowed()
- delay(POLLING_INTERVAL_MS)
+ .stateIn(scope, SharingStarted.Lazily, false)
+
+ private fun isSatelliteAvailableFlow(sm: SupportedSatelliteManager): Flow<Boolean> =
+ conflatedCallbackFlow {
+ val callback = SatelliteCommunicationAllowedStateCallback { allowed ->
+ logBuffer.i({ bool1 = allowed }) {
+ "onSatelliteCommunicationAllowedStateChanged: $bool1"
+ }
+
+ trySend(allowed)
+ }
+
+ var registered = false
+ try {
+ sm.registerForCommunicationAllowedStateChanged(
+ bgDispatcher.asExecutor(),
+ callback
+ )
+ registered = true
+ } catch (e: Exception) {
+ logBuffer.e("Error calling registerForCommunicationAllowedStateChanged", e)
+ }
+
+ awaitClose {
+ if (registered) {
+ sm.unregisterForCommunicationAllowedStateChanged(callback)
}
}
}
- }
+ .flowOn(bgDispatcher)
/**
* Register a callback with [SatelliteManager] to let us know if there is a change in satellite
@@ -410,14 +410,6 @@
}
}
- /**
- * Signal that we should start polling [checkIsSatelliteAllowed]. We only need to poll if there
- * are active listeners to [isSatelliteAllowedForCurrentLocation]
- */
- @SuppressWarnings("unused")
- private fun isSatelliteAllowedHasListener(sm: SupportedSatelliteManager): Flow<Boolean> =
- isSatelliteAllowedForCurrentLocation.subscriptionCount.map { it > 0 }.distinctUntilChanged()
-
override val connectionState =
satelliteSupport
.whenSupported(
@@ -485,28 +477,6 @@
}
.flowOn(bgDispatcher)
- /** Fire off a request to check for satellite availability. Always runs on the bg context */
- private suspend fun checkIsSatelliteAllowed() =
- withContext(bgDispatcher) {
- satelliteManager?.requestIsCommunicationAllowedForCurrentLocation(
- bgDispatcher.asExecutor(),
- object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
- override fun onError(e: SatelliteManager.SatelliteException) {
- logBuffer.e(
- "Found exception when checking availability",
- e,
- )
- isSatelliteAllowedForCurrentLocation.value = false
- }
-
- override fun onResult(allowed: Boolean) {
- logBuffer.i { "isSatelliteAllowedForCurrentLocation: $allowed" }
- isSatelliteAllowedForCurrentLocation.value = allowed
- }
- }
- )
- }
-
private suspend fun SatelliteManager.checkSatelliteSupported(): SatelliteSupport =
suspendCancellableCoroutine { continuation ->
val cb =
@@ -546,9 +516,6 @@
}
companion object {
- // TTL for satellite polling is twenty minutes
- const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 20
-
// Let the system boot up and stabilize before we check for system support
const val MIN_UPTIME: Long = 1000 * 60
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
index f693409..21ec14f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
@@ -289,6 +289,7 @@
labelRes = R.string.quick_settings_work_mode_label,
),
instanceId = uiEventLogger.getNewInstanceId(),
+ autoRemoveOnUnavailable = false,
)
/** Inject work mode into tileViewModelMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt b/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt
new file mode 100644
index 0000000..c86ac2f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.systemui.touchpad
+
+import com.android.systemui.touchpad.data.repository.TouchpadRepository
+import com.android.systemui.touchpad.data.repository.TouchpadRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+@Module
+abstract class TouchpadModule {
+
+ @Binds
+ abstract fun bindTouchpadRepository(repository: TouchpadRepositoryImpl): TouchpadRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt b/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt
new file mode 100644
index 0000000..7131546
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.systemui.touchpad.data.repository
+
+import android.hardware.input.InputManager
+import android.view.InputDevice.SOURCE_TOUCHPAD
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.inputdevice.data.repository.InputDeviceRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+interface TouchpadRepository {
+ /** Emits true if any touchpad is connected to the device, false otherwise. */
+ val isAnyTouchpadConnected: Flow<Boolean>
+}
+
+@SysUISingleton
+class TouchpadRepositoryImpl
+@Inject
+constructor(
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val inputManager: InputManager,
+ inputDeviceRepository: InputDeviceRepository
+) : TouchpadRepository {
+
+ override val isAnyTouchpadConnected: Flow<Boolean> =
+ inputDeviceRepository.deviceChange
+ .map { (ids, _) -> ids.any { id -> isTouchpad(id) } }
+ .distinctUntilChanged()
+ .flowOn(backgroundDispatcher)
+
+ private fun isTouchpad(deviceId: Int): Boolean {
+ val device = inputManager.getInputDevice(deviceId) ?: return false
+ return device.supportsSource(SOURCE_TOUCHPAD)
+ }
+
+ companion object {
+ const val TAG = "TouchpadRepositoryImpl"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index 94ff65e..51dfef0 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -26,6 +26,7 @@
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -49,6 +50,7 @@
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
@@ -61,6 +63,9 @@
import com.airbnb.lottie.compose.rememberLottieDynamicProperty
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.res.R
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK
import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler
@@ -78,23 +83,49 @@
) {
val screenColors = rememberScreenColors()
BackHandler(onBack = onBack)
- var gestureDone by remember { mutableStateOf(false) }
+ var gestureState by remember { mutableStateOf(GestureState.NOT_STARTED) }
val swipeDistanceThresholdPx =
LocalContext.current.resources.getDimensionPixelSize(
com.android.internal.R.dimen.system_gestures_distance_threshold
)
val gestureHandler =
remember(swipeDistanceThresholdPx) {
- TouchpadGestureHandler(BACK, swipeDistanceThresholdPx, onDone = { gestureDone = true })
+ TouchpadGestureHandler(
+ BACK,
+ swipeDistanceThresholdPx,
+ onGestureStateChanged = { gestureState = it }
+ )
}
+ TouchpadGesturesHandlingBox(gestureHandler, gestureState) {
+ GestureTutorialContent(gestureState, onDoneButtonClicked, screenColors)
+ }
+}
+
+@Composable
+private fun TouchpadGesturesHandlingBox(
+ gestureHandler: TouchpadGestureHandler,
+ gestureState: GestureState,
+ modifier: Modifier = Modifier,
+ content: @Composable BoxScope.() -> Unit
+) {
Box(
modifier =
- Modifier.fillMaxSize()
+ modifier
+ .fillMaxSize()
// we need to use pointerInteropFilter because some info about touchpad gestures is
// only available in MotionEvent
- .pointerInteropFilter(onTouchEvent = gestureHandler::onMotionEvent)
+ .pointerInteropFilter(
+ onTouchEvent = { event ->
+ // FINISHED is the final state so we don't need to process touches anymore
+ if (gestureState != FINISHED) {
+ gestureHandler.onMotionEvent(event)
+ } else {
+ false
+ }
+ }
+ )
) {
- GestureTutorialContent(gestureDone, onDoneButtonClicked, screenColors)
+ content()
}
}
@@ -126,14 +157,14 @@
@Composable
private fun GestureTutorialContent(
- gestureDone: Boolean,
+ gestureState: GestureState,
onDoneButtonClicked: () -> Unit,
screenColors: TutorialScreenColors
) {
val animatedColor by
animateColorAsState(
targetValue =
- if (gestureDone) screenColors.successBackgroundColor
+ if (gestureState == FINISHED) screenColors.successBackgroundColor
else screenColors.backgroundColor,
animationSpec = tween(durationMillis = 150, easing = LinearEasing),
label = "backgroundColor"
@@ -148,15 +179,17 @@
Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
TutorialDescription(
titleTextId =
- if (gestureDone) R.string.touchpad_tutorial_gesture_done
+ if (gestureState == FINISHED) R.string.touchpad_tutorial_gesture_done
else R.string.touchpad_back_gesture_action_title,
titleColor = screenColors.titleColor,
- bodyTextId = R.string.touchpad_back_gesture_guidance,
+ bodyTextId =
+ if (gestureState == FINISHED) R.string.touchpad_back_gesture_finished
+ else R.string.touchpad_back_gesture_guidance,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(76.dp))
TutorialAnimation(
- gestureDone,
+ gestureState,
screenColors.animationProperties,
modifier = Modifier.weight(1f).padding(top = 8.dp)
)
@@ -189,27 +222,38 @@
@Composable
fun TutorialAnimation(
- gestureDone: Boolean,
+ gestureState: GestureState,
animationProperties: LottieDynamicProperties,
modifier: Modifier = Modifier
) {
Column(modifier = modifier.fillMaxWidth()) {
- val resId = if (gestureDone) R.raw.trackpad_back_success else R.raw.trackpad_back_edu
+ val resId =
+ if (gestureState == FINISHED) R.raw.trackpad_back_success else R.raw.trackpad_back_edu
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(resId))
- val progress by
- animateLottieCompositionAsState(
- composition,
- iterations = if (gestureDone) 1 else LottieConstants.IterateForever
- )
+ val progress = progressForGestureState(composition, gestureState)
LottieAnimation(
composition = composition,
- progress = { progress },
+ progress = progress,
dynamicProperties = animationProperties
)
}
}
@Composable
+private fun progressForGestureState(
+ composition: LottieComposition?,
+ gestureState: GestureState
+): () -> Float {
+ if (gestureState == IN_PROGRESS) {
+ return { 0f } // when gesture is in progress, animation should freeze on 1st frame
+ } else {
+ val iterations = if (gestureState == FINISHED) 1 else LottieConstants.IterateForever
+ val animationState by animateLottieCompositionAsState(composition, iterations = iterations)
+ return { animationState }
+ }
+}
+
+@Composable
fun rememberColorFilterProperty(
layerName: String,
color: Color
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
index 1fa7a0c..6fa9bcd 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
@@ -17,23 +17,26 @@
package com.android.systemui.touchpad.tutorial.ui.gesture
import android.view.MotionEvent
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
import kotlin.math.abs
/**
- * Monitor for touchpad gestures that calls [gestureDoneCallback] when gesture was successfully
- * done. All tracked motion events should be passed to [processTouchpadEvent]
+ * Monitor for touchpad gestures that calls [gestureStateChangedCallback] when [GestureState]
+ * changes. All tracked motion events should be passed to [processTouchpadEvent]
*/
interface TouchpadGestureMonitor {
val gestureDistanceThresholdPx: Int
- val gestureDoneCallback: () -> Unit
+ val gestureStateChangedCallback: (GestureState) -> Unit
fun processTouchpadEvent(event: MotionEvent)
}
class BackGestureMonitor(
override val gestureDistanceThresholdPx: Int,
- override val gestureDoneCallback: () -> Unit
+ override val gestureStateChangedCallback: (GestureState) -> Unit
) : TouchpadGestureMonitor {
private var xStart = 0f
@@ -44,13 +47,16 @@
MotionEvent.ACTION_DOWN -> {
if (isThreeFingerTouchpadSwipe(event)) {
xStart = event.x
+ gestureStateChangedCallback(IN_PROGRESS)
}
}
MotionEvent.ACTION_UP -> {
if (isThreeFingerTouchpadSwipe(event)) {
val distance = abs(event.x - xStart)
if (distance >= gestureDistanceThresholdPx) {
- gestureDoneCallback()
+ gestureStateChangedCallback(FINISHED)
+ } else {
+ gestureStateChangedCallback(NOT_STARTED)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
new file mode 100644
index 0000000..446875a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.systemui.touchpad.tutorial.ui.gesture
+
+enum class GestureState {
+ NOT_STARTED,
+ IN_PROGRESS,
+ FINISHED
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
index 4ae9c7b..190da62 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
@@ -22,10 +22,10 @@
fun toMonitor(
swipeDistanceThresholdPx: Int,
- gestureDoneCallback: () -> Unit
+ onStateChanged: (GestureState) -> Unit
): TouchpadGestureMonitor {
return when (this) {
- BACK -> BackGestureMonitor(swipeDistanceThresholdPx, gestureDoneCallback)
+ BACK -> BackGestureMonitor(swipeDistanceThresholdPx, onStateChanged)
else -> throw IllegalArgumentException("Not implemented yet")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
index dc8471c..cac2a99 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
@@ -26,11 +26,11 @@
class TouchpadGestureHandler(
touchpadGesture: TouchpadGesture,
swipeDistanceThresholdPx: Int,
- onDone: () -> Unit
+ onGestureStateChanged: (GestureState) -> Unit
) {
private val gestureRecognition =
- touchpadGesture.toMonitor(swipeDistanceThresholdPx, gestureDoneCallback = onDone)
+ touchpadGesture.toMonitor(swipeDistanceThresholdPx, onStateChanged = onGestureStateChanged)
fun onMotionEvent(event: MotionEvent): Boolean {
// events from touchpad have SOURCE_MOUSE and not SOURCE_TOUCHPAD because of legacy reasons
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
index efaca7a..5d8b6f1 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
@@ -19,8 +19,8 @@
import android.content.ContentResolver
import android.content.Context
import android.media.AudioManager
-import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.flags.Flags
import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor
import com.android.settingslib.volume.data.repository.AudioRepository
import com.android.settingslib.volume.data.repository.AudioRepositoryImpl
@@ -80,7 +80,7 @@
@Application coroutineScope: CoroutineScope,
@Background coroutineContext: CoroutineContext,
): AudioSharingRepository =
- if (BluetoothUtils.isAudioSharingEnabled() && localBluetoothManager != null) {
+ if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
AudioSharingRepositoryImpl(
contentResolver,
localBluetoothManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
index e01744e..6a43a61 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
@@ -20,6 +20,7 @@
import android.content.ContextWrapper
import android.content.SharedPreferences
import android.os.Handler
+import android.platform.test.annotations.DisableFlags
import android.provider.Settings
import android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS
import android.provider.Settings.Global.ZEN_MODE_OFF
@@ -61,6 +62,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@DisableFlags(android.app.Flags.FLAG_MODES_UI)
class DndTileTest : SysuiTestCase() {
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
index 0505727..689fc7c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
@@ -28,7 +28,11 @@
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING
import com.android.systemui.SysuiTestCase
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.RankingBuilder
import com.android.systemui.statusbar.StatusBarState
@@ -45,6 +49,7 @@
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
+import com.android.systemui.testKosmos
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
@@ -52,6 +57,7 @@
import com.android.systemui.util.mockito.withArgCaptor
import dagger.BindsInstance
import dagger.Component
+import kotlinx.coroutines.CoroutineScope
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -64,6 +70,8 @@
@RunWith(AndroidJUnit4::class)
class SensitiveContentCoordinatorTest : SysuiTestCase() {
+ val kosmos = testKosmos()
+
val dynamicPrivacyController: DynamicPrivacyController = mock()
val lockscreenUserManager: NotificationLockscreenUserManager = mock()
val pipeline: NotifPipeline = mock()
@@ -73,6 +81,8 @@
val mSelectedUserInteractor: SelectedUserInteractor = mock()
val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController =
mock()
+ val deviceEntryInteractor: DeviceEntryInteractor = mock()
+ val sceneInteractor: SceneInteractor = mock()
val coordinator: SensitiveContentCoordinator =
DaggerTestSensitiveContentCoordinatorComponent.factory()
@@ -83,7 +93,10 @@
statusBarStateController,
keyguardStateController,
mSelectedUserInteractor,
- sensitiveNotificationProtectionController
+ sensitiveNotificationProtectionController,
+ deviceEntryInteractor,
+ sceneInteractor,
+ kosmos.applicationCoroutineScope,
)
.coordinator
@@ -136,8 +149,7 @@
@Test
@EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
fun screenshareSecretFilter_sensitiveInctive_noFiltersSecret() {
- whenever(sensitiveNotificationProtectionController.isSensitiveStateActive)
- .thenReturn(false)
+ whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(false)
coordinator.attach(pipeline)
val filter = withArgCaptor<NotifFilter> { verify(pipeline).addFinalizeFilter(capture()) }
@@ -683,10 +695,11 @@
val mockSbn: StatusBarNotification =
mock<StatusBarNotification>().apply { whenever(user).thenReturn(mockUserHandle) }
val mockRow: ExpandableNotificationRow = mock<ExpandableNotificationRow>()
- val mockEntry = mock<NotificationEntry>().apply {
- whenever(sbn).thenReturn(mockSbn)
- whenever(row).thenReturn(mockRow)
- }
+ val mockEntry =
+ mock<NotificationEntry>().apply {
+ whenever(sbn).thenReturn(mockSbn)
+ whenever(row).thenReturn(mockRow)
+ }
whenever(lockscreenUserManager.needsRedaction(mockEntry)).thenReturn(needsRedaction)
whenever(mockEntry.rowExists()).thenReturn(true)
return object : ListEntry("key", 0) {
@@ -737,6 +750,9 @@
@BindsInstance selectedUserInteractor: SelectedUserInteractor,
@BindsInstance
sensitiveNotificationProtectionController: SensitiveNotificationProtectionController,
+ @BindsInstance deviceEntryInteractor: DeviceEntryInteractor,
+ @BindsInstance sceneInteractor: SceneInteractor,
+ @BindsInstance @Application scope: CoroutineScope,
): TestSensitiveContentCoordinatorComponent
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index af5e60e..9b61105 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1068,7 +1068,7 @@
public void testShowBouncerOrKeyguard_needsFullScreen() {
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
KeyguardSecurityModel.SecurityMode.SimPin);
- mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
verify(mCentralSurfaces).hideKeyguard();
verify(mPrimaryBouncerInteractor).show(true);
}
@@ -1084,7 +1084,7 @@
.thenReturn(KeyguardState.LOCKSCREEN);
reset(mCentralSurfaces);
- mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
verify(mPrimaryBouncerInteractor).show(true);
verify(mCentralSurfaces).showKeyguard();
}
@@ -1092,11 +1092,26 @@
@Test
@DisableSceneContainer
public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() {
+ boolean isFalsingReset = false;
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
KeyguardSecurityModel.SecurityMode.SimPin);
when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
- mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
verify(mCentralSurfaces, never()).hideKeyguard();
+ verify(mPrimaryBouncerInteractor).show(true);
+ }
+
+ @Test
+ @DisableSceneContainer
+ public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing_onFalsing() {
+ boolean isFalsingReset = true;
+ when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
+ KeyguardSecurityModel.SecurityMode.SimPin);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
+ verify(mCentralSurfaces, never()).hideKeyguard();
+
+ // Do not refresh the full screen bouncer if the call is from falsing
verify(mPrimaryBouncerInteractor, never()).show(true);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index 73ac6e3..af4f647 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -22,6 +22,7 @@
import android.telephony.TelephonyManager
import android.telephony.satellite.NtnSignalStrength
import android.telephony.satellite.NtnSignalStrengthCallback
+import android.telephony.satellite.SatelliteCommunicationAllowedStateCallback
import android.telephony.satellite.SatelliteManager
import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED
import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING
@@ -44,7 +45,6 @@
import com.android.systemui.log.core.FakeLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers
import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME
-import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
@@ -54,11 +54,8 @@
import java.util.Optional
import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -71,6 +68,7 @@
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.doThrow
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
@@ -186,152 +184,86 @@
}
@Test
- fun isSatelliteAllowed_readsSatelliteManagerState_enabled() =
+ fun isSatelliteAllowed_listensToSatelliteManagerCallback() =
testScope.runTest {
setupDefaultRepo()
- // GIVEN satellite is allowed in this location
- val allowed = true
-
- doAnswer {
- val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
- receiver.onResult(allowed)
- null
- }
- .`when`(satelliteManager)
- .requestIsCommunicationAllowedForCurrentLocation(
- any(),
- any<OutcomeReceiver<Boolean, SatelliteException>>()
- )
val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+ runCurrent()
- assertThat(latest).isTrue()
- }
-
- @Test
- fun isSatelliteAllowed_readsSatelliteManagerState_disabled() =
- testScope.runTest {
- setupDefaultRepo()
- // GIVEN satellite is not allowed in this location
- val allowed = false
-
- doAnswer {
- val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
- receiver.onResult(allowed)
- null
+ val callback =
+ withArgCaptor<SatelliteCommunicationAllowedStateCallback> {
+ verify(satelliteManager)
+ .registerForCommunicationAllowedStateChanged(any(), capture())
}
- .`when`(satelliteManager)
- .requestIsCommunicationAllowedForCurrentLocation(
- any(),
- any<OutcomeReceiver<Boolean, SatelliteException>>()
- )
- val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+ // WHEN satellite manager says it's not available
+ callback.onSatelliteCommunicationAllowedStateChanged(false)
- assertThat(latest).isFalse()
- }
-
- @Test
- fun isSatelliteAllowed_pollsOnTimeout() =
- testScope.runTest {
- setupDefaultRepo()
- // GIVEN satellite is not allowed in this location
- var allowed = false
-
- doAnswer {
- val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
- receiver.onResult(allowed)
- null
- }
- .`when`(satelliteManager)
- .requestIsCommunicationAllowedForCurrentLocation(
- any(),
- any<OutcomeReceiver<Boolean, SatelliteException>>()
- )
-
- val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
-
+ // THEN it's not!
assertThat(latest).isFalse()
- // WHEN satellite becomes enabled
- allowed = true
+ // WHEN satellite manager says it's changed to available
+ callback.onSatelliteCommunicationAllowedStateChanged(true)
- // WHEN the timeout has not yet been reached
- advanceTimeBy(POLLING_INTERVAL_MS / 2)
-
- // THEN the value is still false
- assertThat(latest).isFalse()
-
- // WHEN time advances beyond the polling interval
- advanceTimeBy(POLLING_INTERVAL_MS / 2 + 1)
-
- // THEN then new value is emitted
+ // THEN it is!
assertThat(latest).isTrue()
}
@Test
- fun isSatelliteAllowed_pollingRestartsWhenCollectionRestarts() =
- testScope.runTest {
- setupDefaultRepo()
- // Use the old school launch/cancel so we can simulate subscribers arriving and leaving
-
- var latest: Boolean? = false
- var job =
- underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this)
-
- // GIVEN satellite is not allowed in this location
- var allowed = false
-
- doAnswer {
- val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
- receiver.onResult(allowed)
- null
- }
- .`when`(satelliteManager)
- .requestIsCommunicationAllowedForCurrentLocation(
- any(),
- any<OutcomeReceiver<Boolean, SatelliteException>>()
- )
-
- assertThat(latest).isFalse()
-
- // WHEN satellite becomes enabled
- allowed = true
-
- // WHEN the job is restarted
- advanceTimeBy(POLLING_INTERVAL_MS / 2)
-
- job.cancel()
- job =
- underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this)
-
- // THEN the value is re-fetched
- assertThat(latest).isTrue()
-
- job.cancel()
- }
-
- @Test
fun isSatelliteAllowed_falseWhenErrorOccurs() =
testScope.runTest {
setupDefaultRepo()
- doAnswer {
- val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
- receiver.onError(SatelliteException(1 /* unused */))
- null
- }
- .`when`(satelliteManager)
- .requestIsCommunicationAllowedForCurrentLocation(
- any(),
- any<OutcomeReceiver<Boolean, SatelliteException>>()
- )
+ // GIVEN SatelliteManager gon' throw exceptions when we ask to register the callback
+ doThrow(RuntimeException("Test exception"))
+ .`when`(satelliteManager)
+ .registerForCommunicationAllowedStateChanged(any(), any())
+
+ // WHEN the latest value is requested (and thus causes an exception to be thrown)
val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+ // THEN the value is just false, and we didn't crash!
assertThat(latest).isFalse()
}
@Test
+ fun isSatelliteAllowed_reRegistersOnTelephonyProcessCrash() =
+ testScope.runTest {
+ setupDefaultRepo()
+ val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+ runCurrent()
+
+ val callback =
+ withArgCaptor<SatelliteCommunicationAllowedStateCallback> {
+ verify(satelliteManager)
+ .registerForCommunicationAllowedStateChanged(any(), capture())
+ }
+
+ val telephonyCallback =
+ MobileTelephonyHelpers.getTelephonyCallbackForType<
+ TelephonyCallback.RadioPowerStateListener
+ >(
+ telephonyManager
+ )
+
+ // GIVEN satellite is currently provisioned
+ callback.onSatelliteCommunicationAllowedStateChanged(true)
+
+ assertThat(latest).isTrue()
+
+ // WHEN a crash event happens (detected by radio state change)
+ telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_ON)
+ runCurrent()
+ telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_OFF)
+ runCurrent()
+
+ // THEN listener is re-registered
+ verify(satelliteManager, times(2))
+ .registerForCommunicationAllowedStateChanged(any(), any())
+ }
+
+ @Test
fun satelliteProvisioned_notSupported_defaultFalse() =
testScope.runTest {
// GIVEN satellite is not supported
@@ -363,24 +295,21 @@
testScope.runTest {
// GIVEN satellite is supported on device
doAnswer {
- val callback: OutcomeReceiver<Boolean, SatelliteException> =
- it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
- callback.onResult(true)
- }
+ val callback: OutcomeReceiver<Boolean, SatelliteException> =
+ it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
+ callback.onResult(true)
+ }
.whenever(satelliteManager)
.requestIsSupported(any(), any())
// GIVEN satellite returns an error when asked if provisioned
doAnswer {
- val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
- receiver.onError(SatelliteException(SATELLITE_RESULT_ERROR))
- null
- }
+ val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+ receiver.onError(SatelliteException(SATELLITE_RESULT_ERROR))
+ null
+ }
.whenever(satelliteManager)
- .requestIsProvisioned(
- any(),
- any<OutcomeReceiver<Boolean, SatelliteException>>()
- )
+ .requestIsProvisioned(any(), any<OutcomeReceiver<Boolean, SatelliteException>>())
// GIVEN we've been up long enough to start querying
systemClock.setUptimeMillis(Process.getStartUptimeMillis() + MIN_UPTIME)
@@ -409,10 +338,10 @@
testScope.runTest {
// GIVEN satellite is supported on device
doAnswer {
- val callback: OutcomeReceiver<Boolean, SatelliteException> =
- it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
- callback.onResult(true)
- }
+ val callback: OutcomeReceiver<Boolean, SatelliteException> =
+ it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
+ callback.onResult(true)
+ }
.whenever(satelliteManager)
.requestIsSupported(any(), any())
@@ -779,10 +708,10 @@
.requestIsSupported(any(), any())
doAnswer {
- val callback: OutcomeReceiver<Boolean, SatelliteException> =
- it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
- callback.onResult(initialSatelliteIsProvisioned)
- }
+ val callback: OutcomeReceiver<Boolean, SatelliteException> =
+ it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
+ callback.onResult(initialSatelliteIsProvisioned)
+ }
.whenever(satelliteManager)
.requestIsProvisioned(any(), any())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt
new file mode 100644
index 0000000..3783af5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt
@@ -0,0 +1,184 @@
+/*
+ * 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.systemui.touchpad.data.repository
+
+import android.hardware.input.FakeInputManager
+import android.hardware.input.InputManager.InputDeviceListener
+import android.hardware.input.fakeInputManager
+import android.testing.TestableLooper
+import android.view.InputDevice
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.inputdevice.data.repository.InputDeviceRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.utils.os.FakeHandler
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Captor
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidJUnit4::class)
+class TouchpadRepositoryTest : SysuiTestCase() {
+
+ @Captor private lateinit var deviceListenerCaptor: ArgumentCaptor<InputDeviceListener>
+ private lateinit var fakeInputManager: FakeInputManager
+
+ private lateinit var underTest: TouchpadRepository
+ private lateinit var dispatcher: CoroutineDispatcher
+ private lateinit var inputDeviceRepo: InputDeviceRepository
+ private lateinit var testScope: TestScope
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ fakeInputManager = testKosmos().fakeInputManager
+ dispatcher = StandardTestDispatcher()
+ testScope = TestScope(dispatcher)
+ val handler = FakeHandler(TestableLooper.get(this).looper)
+ inputDeviceRepo =
+ InputDeviceRepository(handler, testScope.backgroundScope, fakeInputManager.inputManager)
+ underTest =
+ TouchpadRepositoryImpl(dispatcher, fakeInputManager.inputManager, inputDeviceRepo)
+ }
+
+ @Test
+ fun emitsDisconnected_ifNothingIsConnected() =
+ testScope.runTest {
+ val initialState = underTest.isAnyTouchpadConnected.first()
+ assertThat(initialState).isFalse()
+ }
+
+ @Test
+ fun emitsConnected_ifTouchpadAlreadyConnectedAtTheStart() =
+ testScope.runTest {
+ fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+ val initialValue = underTest.isAnyTouchpadConnected.first()
+ assertThat(initialValue).isTrue()
+ }
+
+ @Test
+ fun emitsConnected_whenNewTouchpadConnects() =
+ testScope.runTest {
+ captureDeviceListener()
+ val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+ fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+
+ assertThat(isTouchpadConnected).isTrue()
+ }
+
+ @Test
+ fun emitsDisconnected_whenDeviceWithIdDoesNotExist() =
+ testScope.runTest {
+ captureDeviceListener()
+ val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+ whenever(fakeInputManager.inputManager.getInputDevice(eq(NULL_DEVICE_ID)))
+ .thenReturn(null)
+ fakeInputManager.addDevice(NULL_DEVICE_ID, InputDevice.SOURCE_UNKNOWN)
+ assertThat(isTouchpadConnected).isFalse()
+ }
+
+ @Test
+ fun emitsDisconnected_whenTouchpadDisconnects() =
+ testScope.runTest {
+ captureDeviceListener()
+ val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+ fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+ assertThat(isTouchpadConnected).isTrue()
+
+ fakeInputManager.removeDevice(TOUCHPAD_ID)
+ assertThat(isTouchpadConnected).isFalse()
+ }
+
+ private suspend fun captureDeviceListener() {
+ underTest.isAnyTouchpadConnected.first()
+ Mockito.verify(fakeInputManager.inputManager)
+ .registerInputDeviceListener(deviceListenerCaptor.capture(), anyOrNull())
+ fakeInputManager.registerInputDeviceListener(deviceListenerCaptor.value)
+ }
+
+ @Test
+ fun emitsDisconnected_whenNonTouchpadConnects() =
+ testScope.runTest {
+ captureDeviceListener()
+ val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+ fakeInputManager.addDevice(NON_TOUCHPAD_ID, InputDevice.SOURCE_KEYBOARD)
+ assertThat(isTouchpadConnected).isFalse()
+ }
+
+ @Test
+ fun emitsDisconnected_whenTouchpadDisconnectsAndWasAlreadyConnectedAtTheStart() =
+ testScope.runTest {
+ captureDeviceListener()
+ val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+ fakeInputManager.removeDevice(TOUCHPAD_ID)
+ assertThat(isTouchpadConnected).isFalse()
+ }
+
+ @Test
+ fun emitsConnected_whenAnotherDeviceDisconnects() =
+ testScope.runTest {
+ captureDeviceListener()
+ val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+ fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+ fakeInputManager.removeDevice(NON_TOUCHPAD_ID)
+
+ assertThat(isTouchpadConnected).isTrue()
+ }
+
+ @Test
+ fun emitsConnected_whenOneTouchpadDisconnectsButAnotherRemainsConnected() =
+ testScope.runTest {
+ captureDeviceListener()
+ val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+ fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+ fakeInputManager.addDevice(ANOTHER_TOUCHPAD_ID, TOUCHPAD)
+ fakeInputManager.removeDevice(TOUCHPAD_ID)
+
+ assertThat(isTouchpadConnected).isTrue()
+ }
+
+ private companion object {
+ private const val TOUCHPAD_ID = 1
+ private const val NON_TOUCHPAD_ID = 2
+ private const val ANOTHER_TOUCHPAD_ID = 3
+ private const val NULL_DEVICE_ID = 4
+
+ private const val TOUCHPAD = InputDevice.SOURCE_TOUCHPAD
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
index cf0db7b..8875b84 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
@@ -28,6 +28,9 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -36,16 +39,19 @@
@RunWith(AndroidJUnit4::class)
class BackGestureMonitorTest : SysuiTestCase() {
- private var gestureDoneWasCalled = false
- private val gestureDoneCallback = { gestureDoneWasCalled = true }
- private val gestureMonitor = BackGestureMonitor(SWIPE_DISTANCE.toInt(), gestureDoneCallback)
+ private var gestureState = NOT_STARTED
+ private val gestureMonitor =
+ BackGestureMonitor(
+ gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
+ gestureStateChangedCallback = { gestureState = it }
+ )
companion object {
const val SWIPE_DISTANCE = 100f
}
@Test
- fun triggersGestureDoneForThreeFingerGestureRight() {
+ fun triggersGestureFinishedForThreeFingerGestureRight() {
val events =
listOf(
threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f),
@@ -59,11 +65,11 @@
events.forEach { gestureMonitor.processTouchpadEvent(it) }
- assertThat(gestureDoneWasCalled).isTrue()
+ assertThat(gestureState).isEqualTo(FINISHED)
}
@Test
- fun triggersGestureDoneForThreeFingerGestureLeft() {
+ fun triggersGestureFinishedForThreeFingerGestureLeft() {
val events =
listOf(
threeFingerEvent(ACTION_DOWN, x = SWIPE_DISTANCE, y = 0f),
@@ -77,7 +83,21 @@
events.forEach { gestureMonitor.processTouchpadEvent(it) }
- assertThat(gestureDoneWasCalled).isTrue()
+ assertThat(gestureState).isEqualTo(FINISHED)
+ }
+
+ @Test
+ fun triggersGestureProgressForThreeFingerGestureStarted() {
+ val events =
+ listOf(
+ threeFingerEvent(ACTION_DOWN, x = SWIPE_DISTANCE, y = 0f),
+ threeFingerEvent(ACTION_POINTER_DOWN, x = SWIPE_DISTANCE, y = 0f),
+ threeFingerEvent(ACTION_POINTER_DOWN, x = SWIPE_DISTANCE, y = 0f),
+ )
+
+ events.forEach { gestureMonitor.processTouchpadEvent(it) }
+
+ assertThat(gestureState).isEqualTo(IN_PROGRESS)
}
private fun threeFingerEvent(action: Int, x: Float, y: Float): MotionEvent {
@@ -91,7 +111,7 @@
}
@Test
- fun doesntTriggerGestureDone_onThreeFingersSwipeUp() {
+ fun doesntTriggerGestureFinished_onThreeFingersSwipeUp() {
val events =
listOf(
threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f),
@@ -105,11 +125,11 @@
events.forEach { gestureMonitor.processTouchpadEvent(it) }
- assertThat(gestureDoneWasCalled).isFalse()
+ assertThat(gestureState).isEqualTo(NOT_STARTED)
}
@Test
- fun doesntTriggerGestureDone_onTwoFingersSwipe() {
+ fun doesntTriggerGestureFinished_onTwoFingersSwipe() {
fun twoFingerEvent(action: Int, x: Float, y: Float) =
motionEvent(
action = action,
@@ -127,11 +147,11 @@
events.forEach { gestureMonitor.processTouchpadEvent(it) }
- assertThat(gestureDoneWasCalled).isFalse()
+ assertThat(gestureState).isEqualTo(NOT_STARTED)
}
@Test
- fun doesntTriggerGestureDone_onFourFingersSwipe() {
+ fun doesntTriggerGestureFinished_onFourFingersSwipe() {
fun fourFingerEvent(action: Int, x: Float, y: Float) =
motionEvent(
action = action,
@@ -155,6 +175,6 @@
events.forEach { gestureMonitor.processTouchpadEvent(it) }
- assertThat(gestureDoneWasCalled).isFalse()
+ assertThat(gestureState).isEqualTo(NOT_STARTED)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
index 769f264..dc4d5f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
@@ -32,6 +32,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -41,8 +43,8 @@
@RunWith(AndroidJUnit4::class)
class TouchpadGestureHandlerTest : SysuiTestCase() {
- private var gestureDone = false
- private val handler = TouchpadGestureHandler(BACK, SWIPE_DISTANCE) { gestureDone = true }
+ private var gestureState = NOT_STARTED
+ private val handler = TouchpadGestureHandler(BACK, SWIPE_DISTANCE) { gestureState = it }
companion object {
const val SWIPE_DISTANCE = 100
@@ -84,7 +86,7 @@
fun triggersGestureDoneForThreeFingerGesture() {
backGestureEvents().forEach { handler.onMotionEvent(it) }
- assertThat(gestureDone).isTrue()
+ assertThat(gestureState).isEqualTo(FINISHED)
}
private fun backGestureEvents(): List<MotionEvent> {
diff --git a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
index 6e7c05c..ee36cad 100644
--- a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
+++ b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
@@ -16,6 +16,7 @@
package android.hardware.input
+import android.hardware.input.InputManager.InputDeviceListener
import android.view.InputDevice
import android.view.KeyCharacterMap
import android.view.KeyCharacterMap.VIRTUAL_KEYBOARD
@@ -47,6 +48,8 @@
VIRTUAL_KEYBOARD to allKeyCodes.toMutableSet()
)
+ private var inputDeviceListener: InputDeviceListener? = null
+
val inputManager =
mock<InputManager> {
whenever(getInputDevice(anyInt())).thenAnswer { invocation ->
@@ -84,6 +87,11 @@
addPhysicalKeyboard(deviceId, enabled)
}
+ fun registerInputDeviceListener(listener: InputDeviceListener) {
+ // TODO (b/355422259): handle this by listening to inputManager.registerInputDeviceListener
+ inputDeviceListener = listener
+ }
+
fun addPhysicalKeyboard(id: Int, enabled: Boolean = true) {
check(id > 0) { "Physical keyboard ids have to be > 0" }
addKeyboard(id, enabled)
@@ -106,6 +114,16 @@
supportedKeyCodesByDeviceId[id] = allKeyCodes.toMutableSet()
}
+ fun addDevice(id: Int, sources: Int) {
+ devices[id] = InputDevice.Builder().setId(id).setSources(sources).build()
+ inputDeviceListener?.onInputDeviceAdded(id)
+ }
+
+ fun removeDevice(id: Int) {
+ devices.remove(id)
+ inputDeviceListener?.onInputDeviceRemoved(id)
+ }
+
private fun InputDevice.copy(
id: Int = getId(),
type: Int = keyboardType,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
index 2ca338a..f3d5b7d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
@@ -33,7 +33,7 @@
private var _userHandle: UserHandle = UserHandle.of(_userId),
private var _userInfo: UserInfo = mock(),
private var _userProfiles: List<UserInfo> = emptyList(),
- userContentResolver: ContentResolver = MockContentResolver(),
+ userContentResolverProvider: () -> ContentResolver = { MockContentResolver() },
userContext: Context = mock(),
private val onCreateCurrentUserContext: (Context) -> Context = { mock() },
) : UserTracker {
@@ -41,14 +41,19 @@
override val userId: Int
get() = _userId
+
override val userHandle: UserHandle
get() = _userHandle
+
override val userInfo: UserInfo
get() = _userInfo
+
override val userProfiles: List<UserInfo>
get() = _userProfiles
- override val userContentResolver: ContentResolver = userContentResolver
+ // userContentResolver is lazy because Ravenwood doesn't support MockContentResolver()
+ // and we still want to allow people use this class for tests that don't use it.
+ override val userContentResolver: ContentResolver by lazy { userContentResolverProvider() }
override val userContext: Context = userContext
override fun addCallback(callback: UserTracker.Callback, executor: Executor) {
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
index 8fe6853..1a15d7a 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
@@ -25,19 +25,24 @@
import static android.os.ParcelFileDescriptor.MODE_WORLD_WRITEABLE;
import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
import com.android.internal.annotations.GuardedBy;
import com.android.ravenwood.common.JvmWorkaround;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.Map;
public class ParcelFileDescriptor_host {
+ private static final String TAG = "ParcelFileDescriptor_host";
+
/**
* Since we don't have a great way to keep an unmanaged {@code FileDescriptor} reference
* alive, we keep a strong reference to the {@code RandomAccessFile} we used to open it. This
@@ -98,16 +103,18 @@
synchronized (sActive) {
raf = sActive.remove(fd);
}
+ int fdInt = JvmWorkaround.getInstance().getFdInt(fd);
try {
if (raf != null) {
raf.close();
} else {
- // Odd, we don't remember opening this ourselves, but let's release the
- // underlying resource as requested
- System.err.println("Closing unknown FileDescriptor: " + fd);
- new FileOutputStream(fd).close();
+ // This FD wasn't created by native_open$ravenwood().
+ // The FD was passed to the PFD ctor. Just close it.
+ Os.close(fd);
}
- } catch (IOException ignored) {
+ } catch (IOException | ErrnoException e) {
+ Log.w(TAG, "Exception thrown while closing fd " + fdInt, e);
}
}
}
+;
\ No newline at end of file
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
index 22e11e1..2df93cd 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
@@ -15,6 +15,11 @@
*/
package com.android.platform.test.ravenwood.nativesubstitution;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.FileDescriptor;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@@ -31,6 +36,8 @@
* {@link ByteBuffer} wouldn't allow...)
*/
public class Parcel_host {
+ private static final String TAG = "Parcel";
+
private Parcel_host() {
}
@@ -50,6 +57,11 @@
// TODO Use the actual value from Parcel.java.
private static final int OK = 0;
+ private final Map<Integer, FileDescriptor> mFdMap = new ConcurrentHashMap<>();
+
+ private static final int FD_PLACEHOLDER = 0xDEADBEEF;
+ private static final int FD_PAYLOAD_SIZE = 8;
+
private void validate() {
if (mDeleted) {
// TODO: Put more info
@@ -67,6 +79,7 @@
return p;
}
+ /** Native method substitution */
public static long nativeCreate() {
final long id = sNextId.getAndIncrement();
final Parcel_host p = new Parcel_host();
@@ -80,7 +93,8 @@
mSize = 0;
mPos = 0;
mSensitive = false;
- mAllowFds = false;
+ mAllowFds = true;
+ mFdMap.clear();
}
private void updateSize() {
@@ -89,16 +103,19 @@
}
}
+ /** Native method substitution */
public static void nativeDestroy(long nativePtr) {
getInstance(nativePtr).mDeleted = true;
sInstances.remove(nativePtr);
}
+ /** Native method substitution */
public static void nativeFreeBuffer(long nativePtr) {
getInstance(nativePtr).freeBuffer();
}
- public void freeBuffer() {
+ /** Native method substitution */
+ private void freeBuffer() {
init();
}
@@ -137,32 +154,47 @@
}
}
+ /** Native method substitution */
public static void nativeMarkSensitive(long nativePtr) {
getInstance(nativePtr).mSensitive = true;
}
+
+ /** Native method substitution */
public static int nativeDataSize(long nativePtr) {
return getInstance(nativePtr).mSize;
}
+
+ /** Native method substitution */
public static int nativeDataAvail(long nativePtr) {
var p = getInstance(nativePtr);
return p.mSize - p.mPos;
}
+
+ /** Native method substitution */
public static int nativeDataPosition(long nativePtr) {
return getInstance(nativePtr).mPos;
}
+
+ /** Native method substitution */
public static int nativeDataCapacity(long nativePtr) {
return getInstance(nativePtr).mBuffer.length;
}
+
+ /** Native method substitution */
public static void nativeSetDataSize(long nativePtr, int size) {
var p = getInstance(nativePtr);
p.ensureCapacity(size);
getInstance(nativePtr).mSize = size;
}
+
+ /** Native method substitution */
public static void nativeSetDataPosition(long nativePtr, int pos) {
var p = getInstance(nativePtr);
// TODO: Should this change the size or the capacity??
p.mPos = pos;
}
+
+ /** Native method substitution */
public static void nativeSetDataCapacity(long nativePtr, int size) {
if (size < 0) {
throw new IllegalArgumentException("size < 0: size=" + size);
@@ -173,20 +205,25 @@
}
}
+ /** Native method substitution */
public static boolean nativePushAllowFds(long nativePtr, boolean allowFds) {
var p = getInstance(nativePtr);
var prev = p.mAllowFds;
p.mAllowFds = allowFds;
return prev;
}
+
+ /** Native method substitution */
public static void nativeRestoreAllowFds(long nativePtr, boolean lastValue) {
getInstance(nativePtr).mAllowFds = lastValue;
}
+ /** Native method substitution */
public static void nativeWriteByteArray(long nativePtr, byte[] b, int offset, int len) {
nativeWriteBlob(nativePtr, b, offset, len);
}
+ /** Native method substitution */
public static void nativeWriteBlob(long nativePtr, byte[] b, int offset, int len) {
var p = getInstance(nativePtr);
@@ -205,6 +242,7 @@
}
}
+ /** Native method substitution */
public static int nativeWriteInt(long nativePtr, int value) {
var p = getInstance(nativePtr);
p.ensureMoreCapacity(Integer.BYTES);
@@ -219,14 +257,19 @@
return OK;
}
+ /** Native method substitution */
public static int nativeWriteLong(long nativePtr, long value) {
nativeWriteInt(nativePtr, (int) (value >>> 32));
nativeWriteInt(nativePtr, (int) (value));
return OK;
}
+
+ /** Native method substitution */
public static int nativeWriteFloat(long nativePtr, float val) {
return nativeWriteInt(nativePtr, Float.floatToIntBits(val));
}
+
+ /** Native method substitution */
public static int nativeWriteDouble(long nativePtr, double val) {
return nativeWriteLong(nativePtr, Double.doubleToLongBits(val));
}
@@ -235,6 +278,7 @@
return ((val + 3) / 4) * 4;
}
+ /** Native method substitution */
public static void nativeWriteString8(long nativePtr, String val) {
if (val == null) {
nativeWriteBlob(nativePtr, null, 0, 0);
@@ -243,15 +287,19 @@
nativeWriteBlob(nativePtr, bytes, 0, bytes.length);
}
}
+
+ /** Native method substitution */
public static void nativeWriteString16(long nativePtr, String val) {
// Just reuse String8
nativeWriteString8(nativePtr, val);
}
+ /** Native method substitution */
public static byte[] nativeCreateByteArray(long nativePtr) {
return nativeReadBlob(nativePtr);
}
+ /** Native method substitution */
public static boolean nativeReadByteArray(long nativePtr, byte[] dest, int destLen) {
if (dest == null) {
return false;
@@ -271,6 +319,7 @@
return true;
}
+ /** Native method substitution */
public static byte[] nativeReadBlob(long nativePtr) {
var p = getInstance(nativePtr);
if (p.mSize - p.mPos < 4) {
@@ -295,6 +344,8 @@
return bytes;
}
+
+ /** Native method substitution */
public static int nativeReadInt(long nativePtr) {
var p = getInstance(nativePtr);
@@ -310,19 +361,24 @@
return ret;
}
+
+ /** Native method substitution */
public static long nativeReadLong(long nativePtr) {
return (((long) nativeReadInt(nativePtr)) << 32)
| (((long) nativeReadInt(nativePtr)) & 0xffff_ffffL);
}
+ /** Native method substitution */
public static float nativeReadFloat(long nativePtr) {
return Float.intBitsToFloat(nativeReadInt(nativePtr));
}
+ /** Native method substitution */
public static double nativeReadDouble(long nativePtr) {
return Double.longBitsToDouble(nativeReadLong(nativePtr));
}
+ /** Native method substitution */
public static String nativeReadString8(long nativePtr) {
final var bytes = nativeReadBlob(nativePtr);
if (bytes == null) {
@@ -334,10 +390,13 @@
return nativeReadString8(nativePtr);
}
+ /** Native method substitution */
public static byte[] nativeMarshall(long nativePtr) {
var p = getInstance(nativePtr);
return Arrays.copyOf(p.mBuffer, p.mSize);
}
+
+ /** Native method substitution */
public static void nativeUnmarshall(
long nativePtr, byte[] data, int offset, int length) {
var p = getInstance(nativePtr);
@@ -346,6 +405,8 @@
p.mPos += length;
p.updateSize();
}
+
+ /** Native method substitution */
public static int nativeCompareData(long thisNativePtr, long otherNativePtr) {
var a = getInstance(thisNativePtr);
var b = getInstance(otherNativePtr);
@@ -355,6 +416,8 @@
return -1;
}
}
+
+ /** Native method substitution */
public static boolean nativeCompareDataInRange(
long ptrA, int offsetA, long ptrB, int offsetB, int length) {
var a = getInstance(ptrA);
@@ -368,6 +431,8 @@
return Arrays.equals(Arrays.copyOfRange(a.mBuffer, offsetA, offsetA + length),
Arrays.copyOfRange(b.mBuffer, offsetB, offsetB + length));
}
+
+ /** Native method substitution */
public static void nativeAppendFrom(
long thisNativePtr, long otherNativePtr, int srcOffset, int length) {
var dst = getInstance(thisNativePtr);
@@ -382,25 +447,83 @@
// TODO: Update the other's position?
}
- public static boolean nativeHasFileDescriptors(long nativePtr) {
- // Assume false for now, because we don't support writing FDs yet.
- return false;
- }
-
- public static boolean nativeHasFileDescriptorsInRange(
- long nativePtr, int offset, int length) {
- // Assume false for now, because we don't support writing FDs yet.
- return false;
- }
-
+ /** Native method substitution */
public static boolean nativeHasBinders(long nativePtr) {
// Assume false for now, because we don't support adding binders.
return false;
}
+ /** Native method substitution */
public static boolean nativeHasBindersInRange(
long nativePtr, int offset, int length) {
// Assume false for now, because we don't support writing FDs yet.
return false;
}
-}
+
+ /** Native method substitution */
+ public static void nativeWriteFileDescriptor(long nativePtr, java.io.FileDescriptor val) {
+ var p = getInstance(nativePtr);
+
+ if (!p.mAllowFds) {
+ // Simulate the FDS_NOT_ALLOWED case in frameworks/base/core/jni/android_util_Binder.cpp
+ throw new RuntimeException("Not allowed to write file descriptors here");
+ }
+
+ FileDescriptor dup = null;
+ try {
+ dup = Os.dup(val);
+ } catch (ErrnoException e) {
+ throw new RuntimeException(e);
+ }
+ p.mFdMap.put(p.mPos, dup);
+
+ // Parcel.cpp writes two int32s for a FD.
+ // Make sure FD_PAYLOAD_SIZE is in sync with this code.
+ nativeWriteInt(nativePtr, FD_PLACEHOLDER);
+ nativeWriteInt(nativePtr, FD_PLACEHOLDER);
+ }
+
+ /** Native method substitution */
+ public static java.io.FileDescriptor nativeReadFileDescriptor(long nativePtr) {
+ var p = getInstance(nativePtr);
+
+ var pos = p.mPos;
+ var fd = p.mFdMap.get(pos);
+
+ if (fd == null) {
+ Log.w(TAG, "nativeReadFileDescriptor: Not a FD at pos #" + pos);
+ return null;
+ }
+ nativeReadInt(nativePtr);
+ return fd;
+ }
+
+ /** Native method substitution */
+ public static boolean nativeHasFileDescriptors(long nativePtr) {
+ var p = getInstance(nativePtr);
+ return p.mFdMap.size() > 0;
+ }
+
+ /** Native method substitution */
+ public static boolean nativeHasFileDescriptorsInRange(long nativePtr, int offset, int length) {
+ var p = getInstance(nativePtr);
+
+ // Original code: hasFileDescriptorsInRange() in frameworks/native/libs/binder/Parcel.cpp
+ if (offset < 0 || length < 0) {
+ throw new IllegalArgumentException("Negative value not allowed: offset=" + offset
+ + " length=" + length);
+ }
+ long limit = (long) offset + (long) length;
+ if (limit > p.mSize) {
+ throw new IllegalArgumentException("Out of range: offset=" + offset
+ + " length=" + length + " dataSize=" + p.mSize);
+ }
+
+ for (var pos : p.mFdMap.keySet()) {
+ if (offset <= pos && (pos + FD_PAYLOAD_SIZE - 1) < (offset + length)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
index 8a1fe62..825ab72 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
@@ -53,4 +53,9 @@
public static StructStat stat(String path) throws ErrnoException {
return RavenwoodRuntimeNative.stat(path);
}
+
+ /** Ravenwood version of the OS API. */
+ public static void close(FileDescriptor fd) throws ErrnoException {
+ RavenwoodRuntimeNative.close(fd);
+ }
}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
index e9b305e..2bc8e71 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
@@ -48,6 +48,8 @@
public static native StructStat stat(String path) throws ErrnoException;
+ private static native void nClose(int fd) throws ErrnoException;
+
public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
return nLseek(JvmWorkaround.getInstance().getFdInt(fd), offset, whence);
}
@@ -83,4 +85,11 @@
return nFstat(fdInt);
}
+
+ /** See close(2) */
+ public static void close(FileDescriptor fd) throws ErrnoException {
+ var fdInt = JvmWorkaround.getInstance().getFdInt(fd);
+
+ nClose(fdInt);
+ }
}
diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp
index e0a3e1c..ee84954 100644
--- a/ravenwood/runtime-jni/ravenwood_runtime.cpp
+++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp
@@ -167,6 +167,11 @@
return doStat(env, javaPath, false);
}
+static void nClose(JNIEnv* env, jclass, jint fd) {
+ // Don't use TEMP_FAILURE_RETRY() on close(): https://lkml.org/lkml/2005/9/10/129
+ throwIfMinusOne(env, "close", close(fd));
+}
+
// ---- Registration ----
static const JNINativeMethod sMethods[] =
@@ -179,6 +184,7 @@
{ "nFstat", "(I)Landroid/system/StructStat;", (void*)nFstat },
{ "lstat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_lstat },
{ "stat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_stat },
+ { "nClose", "(I)V", (void*)nClose },
};
extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 30c743e..9067cda 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -41,6 +41,7 @@
import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED;
import static android.view.accessibility.AccessibilityManager.FlashNotificationReason;
+import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME;
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
@@ -57,7 +58,6 @@
import static com.android.internal.util.FunctionalUtils.ignoreRemoteException;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain;
-import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import android.accessibilityservice.AccessibilityGestureEvent;
@@ -825,25 +825,27 @@
@VisibleForTesting
boolean onPackagesForceStoppedLocked(
String[] packages, AccessibilityUserState userState) {
- final List<String> continuousServicePackages =
+ final Set<String> packageSet = new HashSet<>(List.of(packages));
+ final ArrayList<ComponentName> continuousServices = new ArrayList<>(
userState.mInstalledServices.stream().filter(service ->
(service.flags & FLAG_REQUEST_ACCESSIBILITY_BUTTON)
== FLAG_REQUEST_ACCESSIBILITY_BUTTON
- ).map(service -> service.getComponentName().flattenToString()).toList();
+ ).map(AccessibilityServiceInfo::getComponentName).toList());
+
+ // Filter out continuous packages that are not from the array of stopped packages.
+ continuousServices.removeIf(
+ continuousName -> !packageSet.contains(continuousName.getPackageName()));
boolean enabledServicesChanged = false;
final Iterator<ComponentName> it = userState.mEnabledServices.iterator();
while (it.hasNext()) {
final ComponentName comp = it.next();
final String compPkg = comp.getPackageName();
- for (String pkg : packages) {
- if (compPkg.equals(pkg)) {
- it.remove();
- userState.getBindingServicesLocked().remove(comp);
- userState.getCrashedServicesLocked().remove(comp);
- enabledServicesChanged = true;
- break;
- }
+ if (packageSet.contains(compPkg)) {
+ it.remove();
+ userState.getBindingServicesLocked().remove(comp);
+ userState.getCrashedServicesLocked().remove(comp);
+ enabledServicesChanged = true;
}
}
if (enabledServicesChanged) {
@@ -855,8 +857,8 @@
// Remove any button targets that match any stopped continuous services
Set<String> buttonTargets = userState.getShortcutTargetsLocked(SOFTWARE);
boolean buttonTargetsChanged = buttonTargets.removeIf(
- target -> continuousServicePackages.stream().anyMatch(
- pkg -> Objects.equals(target, pkg)));
+ target -> continuousServices.stream().anyMatch(
+ continuousName -> continuousName.flattenToString().equals(target)));
if (buttonTargetsChanged) {
userState.updateShortcutTargetsLocked(buttonTargets, SOFTWARE);
persistColonDelimitedSetToSettingLocked(
@@ -2641,7 +2643,8 @@
}
}
- private <T> void persistColonDelimitedSetToSettingLocked(String settingName, int userId,
+ @VisibleForTesting
+ <T> void persistColonDelimitedSetToSettingLocked(String settingName, int userId,
Set<T> set, Function<T, String> toString) {
persistColonDelimitedSetToSettingLocked(settingName, userId, set,
toString, /* defaultEmptyString= */ null);
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 7f43fae..3123268 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -174,6 +174,7 @@
"haptics",
"hardware_backed_security_mainline",
"input",
+ "incremental",
"llvm_and_toolchains",
"lse_desktop_experience",
"machine_learning",
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index ca69f31..8d8a54e 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -31,6 +31,7 @@
import static android.media.audio.Flags.automaticBtDeviceType;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.media.audio.Flags.asDeviceConnectionFailure;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -529,6 +530,17 @@
}
};
+ /**
+ * package-protected for unit testing only
+ * Returns the currently connected devices
+ * @return the collection of connected devices
+ */
+ /*package*/ @NonNull Collection<DeviceInfo> getConnectedDevices() {
+ synchronized (mDevicesLock) {
+ return mConnectedDevices.values();
+ }
+ }
+
// List of devices actually connected to AudioPolicy (through AudioSystem), only one
// by device type, which is used as the key, value is the DeviceInfo generated key.
// For the moment only for A2DP sink devices.
@@ -598,8 +610,9 @@
/**
* Class to store info about connected devices.
* Use makeDeviceListKey() to make a unique key for this list.
+ * Package-protected for unit tests
*/
- private static class DeviceInfo {
+ /*package*/ static class DeviceInfo {
final int mDeviceType;
final @NonNull String mDeviceName;
final @NonNull String mDeviceAddress;
@@ -762,13 +775,27 @@
// Always executed on AudioDeviceBroker message queue
/*package*/ void onRestoreDevices() {
synchronized (mDevicesLock) {
+ int res;
+ List<DeviceInfo> failedReconnectionDeviceList = new ArrayList<>(/*initialCapacity*/ 0);
//TODO iterate on mApmConnectedDevices instead once it handles all device types
for (DeviceInfo di : mConnectedDevices.values()) {
- mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(di.mDeviceType,
+ res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(
+ di.mDeviceType,
di.mDeviceAddress,
di.mDeviceName),
AudioSystem.DEVICE_STATE_AVAILABLE,
di.mDeviceCodecFormat);
+ if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) {
+ failedReconnectionDeviceList.add(di);
+ }
+ }
+ if (asDeviceConnectionFailure()) {
+ for (DeviceInfo di : failedReconnectionDeviceList) {
+ AudioService.sDeviceLogger.enqueueAndSlog(
+ "Device inventory restore failed to reconnect " + di,
+ EventLogger.Event.ALOGE, TAG);
+ mConnectedDevices.remove(di.getKey(), di);
+ }
}
mAppliedStrategyRolesInt.clear();
mAppliedPresetRolesInt.clear();
@@ -2070,8 +2097,9 @@
"APM failed to make available A2DP device addr="
+ Utils.anonymizeBluetoothAddress(address)
+ " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
- // TODO: connection failed, stop here
- // TODO: return;
+ if (asDeviceConnectionFailure()) {
+ return;
+ }
} else {
AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
"A2DP sink device addr=" + Utils.anonymizeBluetoothAddress(address)
@@ -2336,8 +2364,7 @@
"APM failed to make unavailable A2DP device addr="
+ Utils.anonymizeBluetoothAddress(address)
+ " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
- // TODO: failed to disconnect, stop here
- // TODO: return;
+ // not taking further action: proceeding as if disconnection from APM worked
} else {
AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
"A2DP device addr=" + Utils.anonymizeBluetoothAddress(address)
@@ -2383,8 +2410,9 @@
"APM failed to make available A2DP source device addr="
+ Utils.anonymizeBluetoothAddress(address)
+ " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
- // TODO: connection failed, stop here
- // TODO: return
+ if (asDeviceConnectionFailure()) {
+ return;
+ }
} else {
AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
"A2DP source device addr=" + Utils.anonymizeBluetoothAddress(address)
@@ -2402,6 +2430,7 @@
mAudioSystem.setDeviceConnectionState(ada,
AudioSystem.DEVICE_STATE_UNAVAILABLE,
AudioSystem.AUDIO_FORMAT_DEFAULT);
+ // always remove regardless of the result
mConnectedDevices.remove(
DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address));
mDeviceBroker.postCheckCommunicationDeviceRemoval(ada);
@@ -2418,9 +2447,18 @@
AudioDeviceAttributes ada = new AudioDeviceAttributes(
DEVICE_OUT_HEARING_AID, address, name);
- mAudioSystem.setDeviceConnectionState(ada,
+ final int res = mAudioSystem.setDeviceConnectionState(ada,
AudioSystem.DEVICE_STATE_AVAILABLE,
AudioSystem.AUDIO_FORMAT_DEFAULT);
+ if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) {
+ AudioService.sDeviceLogger.enqueueAndSlog(
+ "APM failed to make available HearingAid addr=" + address
+ + " error=" + res,
+ EventLogger.Event.ALOGE, TAG);
+ return;
+ }
+ AudioService.sDeviceLogger.enqueueAndSlog("HearingAid made available addr=" + address,
+ EventLogger.Event.ALOGI, TAG);
mConnectedDevices.put(
DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address),
new DeviceInfo(DEVICE_OUT_HEARING_AID, name, address));
@@ -2447,6 +2485,7 @@
mAudioSystem.setDeviceConnectionState(ada,
AudioSystem.DEVICE_STATE_UNAVAILABLE,
AudioSystem.AUDIO_FORMAT_DEFAULT);
+ // always remove regardless of return code
mConnectedDevices.remove(
DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address));
// Remove Hearing Aid routes as well
@@ -2540,11 +2579,12 @@
final int res = mAudioSystem.setDeviceConnectionState(ada,
AudioSystem.DEVICE_STATE_AVAILABLE, codec);
if (res != AudioSystem.AUDIO_STATUS_OK) {
- AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
+ AudioService.sDeviceLogger.enqueueAndSlog(
"APM failed to make available LE Audio device addr=" + address
- + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
- // TODO: connection failed, stop here
- // TODO: return;
+ + " error=" + res, EventLogger.Event.ALOGE, TAG);
+ if (asDeviceConnectionFailure()) {
+ return;
+ }
} else {
AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
"LE Audio " + (AudioSystem.isInputDevice(device) ? "source" : "sink")
@@ -2596,8 +2636,7 @@
AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
"APM failed to make unavailable LE Audio device addr=" + address
+ " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
- // TODO: failed to disconnect, stop here
- // TODO: return;
+ // not taking further action: proceeding as if disconnection from APM worked
} else {
AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
"LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address)
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 1183768..ac43e86 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -63,6 +63,7 @@
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.media.audio.Flags.absVolumeIndexFix;
import static com.android.media.audio.Flags.alarmMinVolumeZero;
+import static com.android.media.audio.Flags.asDeviceConnectionFailure;
import static com.android.media.audio.Flags.audioserverPermissions;
import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
import static com.android.media.audio.Flags.replaceStreamBtSco;
@@ -4306,7 +4307,8 @@
super.getVolumeGroupVolumeIndex_enforcePermission();
synchronized (VolumeStreamState.class) {
if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
- throw new IllegalArgumentException("No volume group for id " + groupId);
+ Log.e(TAG, "No volume group for id " + groupId);
+ return 0;
}
VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
// Return 0 when muted, not min index since for e.g. Voice Call, it has a non zero
@@ -4322,7 +4324,8 @@
super.getVolumeGroupMaxVolumeIndex_enforcePermission();
synchronized (VolumeStreamState.class) {
if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
- throw new IllegalArgumentException("No volume group for id " + groupId);
+ Log.e(TAG, "No volume group for id " + groupId);
+ return 0;
}
VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
return vgs.getMaxIndex();
@@ -4336,7 +4339,8 @@
super.getVolumeGroupMinVolumeIndex_enforcePermission();
synchronized (VolumeStreamState.class) {
if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
- throw new IllegalArgumentException("No volume group for id " + groupId);
+ Log.e(TAG, "No volume group for id " + groupId);
+ return 0;
}
VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
return vgs.getMinIndex();
@@ -4765,6 +4769,8 @@
private void dumpFlags(PrintWriter pw) {
pw.println("\nFun with Flags:");
+ pw.println("\tcom.android.media.audio.as_device_connection_failure:"
+ + asDeviceConnectionFailure());
pw.println("\tandroid.media.audio.autoPublicVolumeApiHardening:"
+ autoPublicVolumeApiHardening());
pw.println("\tandroid.media.audio.Flags.automaticBtDeviceType:"
@@ -8259,11 +8265,21 @@
private static final SparseArray<VolumeGroupState> sVolumeGroupStates = new SparseArray<>();
private void initVolumeGroupStates() {
+ int btScoGroupId = -1;
+ VolumeGroupState voiceCallGroup = null;
for (final AudioVolumeGroup avg : getAudioVolumeGroups()) {
try {
- // if no valid attributes, this volume group is not controllable
- if (ensureValidAttributes(avg)) {
- sVolumeGroupStates.append(avg.getId(), new VolumeGroupState(avg));
+ if (ensureValidVolumeGroup(avg)) {
+ final VolumeGroupState vgs = new VolumeGroupState(avg);
+ sVolumeGroupStates.append(avg.getId(), vgs);
+ if (vgs.isVoiceCall()) {
+ voiceCallGroup = vgs;
+ }
+ } else {
+ // invalid volume group will be reported for bt sco group with no other
+ // legacy stream type, we try to replace it in sVolumeGroupStates with the
+ // voice call volume group
+ btScoGroupId = avg.getId();
}
} catch (IllegalArgumentException e) {
// Volume Groups without attributes are not controllable through set/get volume
@@ -8271,10 +8287,15 @@
if (DEBUG_VOL) {
Log.d(TAG, "volume group " + avg.name() + " for internal policy needs");
}
- continue;
}
}
+ if (replaceStreamBtSco() && btScoGroupId >= 0 && voiceCallGroup != null) {
+ // the bt sco group is deprecated, storing the voice call group instead
+ // to keep the code backwards compatible when calling the volume group APIs
+ sVolumeGroupStates.append(btScoGroupId, voiceCallGroup);
+ }
+
// need mSettingsLock for vgs.applyAllVolumes -> vss.setIndex which grabs this lock after
// VSS.class. Locking order needs to be preserved
synchronized (mSettingsLock) {
@@ -8285,7 +8306,15 @@
}
}
- private boolean ensureValidAttributes(AudioVolumeGroup avg) {
+ /**
+ * Returns false if the legacy stream types only contains the deprecated
+ * {@link AudioSystem#STREAM_BLUETOOTH_SCO}.
+ *
+ * @throws IllegalArgumentException if it has more than one non-default {@link AudioAttributes}
+ *
+ * @param avg the volume group to check
+ */
+ private boolean ensureValidVolumeGroup(AudioVolumeGroup avg) {
boolean hasAtLeastOneValidAudioAttributes = avg.getAudioAttributes().stream()
.anyMatch(aa -> !aa.equals(AudioProductStrategy.getDefaultAttributes()));
if (!hasAtLeastOneValidAudioAttributes) {
@@ -8293,10 +8322,11 @@
+ " has no valid audio attributes");
}
if (replaceStreamBtSco()) {
- for (int streamType : avg.getLegacyStreamTypes()) {
- if (streamType == AudioSystem.STREAM_BLUETOOTH_SCO) {
- return false;
- }
+ // if there are multiple legacy stream types associated we can omit stream bt sco
+ // otherwise this is not a valid volume group
+ if (avg.getLegacyStreamTypes().length == 1
+ && avg.getLegacyStreamTypes()[0] == AudioSystem.STREAM_BLUETOOTH_SCO) {
+ return false;
}
}
return true;
@@ -8637,6 +8667,10 @@
return mHasValidStreamType && mPublicStreamType == AudioSystem.STREAM_MUSIC;
}
+ public boolean isVoiceCall() {
+ return mHasValidStreamType && mPublicStreamType == AudioSystem.STREAM_VOICE_CALL;
+ }
+
public void applyAllVolumes(boolean userSwitch) {
String caller = "from vgs";
synchronized (AudioService.VolumeStreamState.class) {
diff --git a/services/core/java/com/android/server/crashrecovery/TEST_MAPPING b/services/core/java/com/android/server/crashrecovery/TEST_MAPPING
index 537fb325..615db34 100644
--- a/services/core/java/com/android/server/crashrecovery/TEST_MAPPING
+++ b/services/core/java/com/android/server/crashrecovery/TEST_MAPPING
@@ -1,9 +1,4 @@
{
- "presubmit": [
- {
- "name": "CrashRecoveryModuleTests"
- }
- ],
"postsubmit": [
{
"name": "FrameworksMockingServicesTests",
@@ -12,6 +7,9 @@
"include-filter": "com.android.server.RescuePartyTest"
}
]
+ },
+ {
+ "name": "CrashRecoveryModuleTests"
}
]
}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index 13209d8..dba0465 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -75,13 +75,13 @@
public abstract void setInteractive(boolean interactive);
/**
- * Hides the input methods for all the users, if visible.
+ * Hides the input method for the specified {@code originatingDisplayId}, if visible.
*
* @param reason the reason for hiding the current input method
* @param originatingDisplayId the display ID the request is originated
*/
@ImfLockFree
- public abstract void hideAllInputMethods(@SoftInputShowHideReason int reason,
+ public abstract void hideInputMethod(@SoftInputShowHideReason int reason,
int originatingDisplayId);
/**
@@ -315,7 +315,7 @@
@ImfLockFree
@Override
- public void hideAllInputMethods(@SoftInputShowHideReason int reason,
+ public void hideInputMethod(@SoftInputShowHideReason int reason,
int originatingDisplayId) {
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 7ff03c2..d9bc467 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -254,7 +254,7 @@
private @interface MultiUserUnawareField {
}
- private static final int MSG_HIDE_ALL_INPUT_METHODS = 1035;
+ private static final int MSG_HIDE_INPUT_METHOD = 1035;
private static final int MSG_REMOVE_IME_SURFACE = 1060;
private static final int MSG_REMOVE_IME_SURFACE_FROM_WINDOW = 1061;
@@ -997,8 +997,6 @@
Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
ioThread.start();
- SecureSettingsWrapper.setContentResolver(context.getContentResolver());
-
return new InputMethodManagerService(context,
shouldEnableConcurrentMultiUserMode(context), thread.getLooper(),
Handler.createAsync(ioThread.getLooper()),
@@ -1057,7 +1055,6 @@
public void onUserRemoved(UserInfo user) {
// Called directly from UserManagerService. Do not block the calling thread.
final int userId = user.id;
- SecureSettingsWrapper.onUserRemoved(userId);
AdditionalSubtypeMapRepository.remove(userId);
InputMethodSettingsRepository.remove(userId);
mService.mUserDataRepository.remove(userId);
@@ -1163,6 +1160,7 @@
mConcurrentMultiUserModeEnabled = concurrentMultiUserModeEnabled;
mContext = context;
mRes = context.getResources();
+ SecureSettingsWrapper.onStart(mContext);
mHandler = Handler.createAsync(uiLooper, this);
mIoHandler = ioHandler;
@@ -1846,13 +1844,6 @@
}
}
- @VisibleForTesting
- void setAttachedClientForTesting(@NonNull ClientState cs) {
- synchronized (ImfLock.class) {
- getUserData(mCurrentUserId).mCurClient = cs;
- }
- }
-
@GuardedBy("ImfLock.class")
private boolean isShowRequestedForCurrentWindow(@UserIdInt int userId) {
final var userData = getUserData(userId);
@@ -5034,7 +5025,7 @@
return;
}
- if (Flags.imeSwitcherRevamp()) {
+ if (mNewInputMethodSwitcherMenuEnabled) {
if (DEBUG) {
Slog.v(TAG, "Show IME switcher menu,"
+ " showAuxSubtypes=" + showAuxSubtypes
@@ -5066,7 +5057,7 @@
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
- case MSG_HIDE_ALL_INPUT_METHODS: {
+ case MSG_HIDE_INPUT_METHOD: {
@SoftInputShowHideReason final int reason = msg.arg1;
final int originatingDisplayId = msg.arg2;
synchronized (ImfLock.class) {
@@ -5802,10 +5793,10 @@
@ImfLockFree
@Override
- public void hideAllInputMethods(@SoftInputShowHideReason int reason,
+ public void hideInputMethod(@SoftInputShowHideReason int reason,
int originatingDisplayId) {
- mHandler.removeMessages(MSG_HIDE_ALL_INPUT_METHODS);
- mHandler.obtainMessage(MSG_HIDE_ALL_INPUT_METHODS, reason, originatingDisplayId)
+ mHandler.removeMessages(MSG_HIDE_INPUT_METHOD);
+ mHandler.obtainMessage(MSG_HIDE_INPUT_METHOD, reason, originatingDisplayId)
.sendToTarget();
}
diff --git a/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java b/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java
index b3500be..c208a5b 100644
--- a/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java
+++ b/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java
@@ -20,7 +20,10 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.app.ActivityManagerInternal;
import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.UserInfo;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -318,13 +321,30 @@
}
/**
- * Called when the system is starting.
+ * Called when {@link InputMethodManagerService} is starting.
*
- * @param contentResolver the {@link ContentResolver} to be used
+ * @param context the {@link Context} to be used.
*/
@AnyThread
- static void setContentResolver(@NonNull ContentResolver contentResolver) {
- sContentResolver = contentResolver;
+ static void onStart(@NonNull Context context) {
+ sContentResolver = context.getContentResolver();
+
+ final int userId = LocalServices.getService(ActivityManagerInternal.class)
+ .getCurrentUserId();
+ final UserManagerInternal userManagerInternal =
+ LocalServices.getService(UserManagerInternal.class);
+ putOrGet(userId, createImpl(userManagerInternal, userId));
+
+ userManagerInternal.addUserLifecycleListener(
+ new UserManagerInternal.UserLifecycleListener() {
+ @Override
+ public void onUserRemoved(UserInfo user) {
+ synchronized (sMutationLock) {
+ sUserMap = sUserMap.cloneWithRemoveOrSelf(user.id);
+ }
+ }
+ }
+ );
}
/**
@@ -357,18 +377,6 @@
}
/**
- * Called when a user is being removed.
- *
- * @param userId the ID of the user whose storage is being removed.
- */
- @AnyThread
- static void onUserRemoved(@UserIdInt int userId) {
- synchronized (sMutationLock) {
- sUserMap = sUserMap.cloneWithRemoveOrSelf(userId);
- }
- }
-
- /**
* Put the given string {@code value} to {@code key}.
*
* @param key a secure settings key.
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 1a8e44b..1fdb57c 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -950,7 +950,7 @@
|| isPackageOrComponentAllowed(component.getPackageName(), userId))) {
return false;
}
- return componentHasBindPermission(component, userId);
+ return isValidService(component, userId);
}
private boolean componentHasBindPermission(ComponentName component, int userId) {
@@ -1302,11 +1302,12 @@
if (TextUtils.equals(getPackageName(approvedPackageOrComponent), packageName)) {
final ComponentName component = ComponentName.unflattenFromString(
approvedPackageOrComponent);
- if (component != null && !componentHasBindPermission(component, userId)) {
+ if (component != null && !isValidService(component, userId)) {
approved.removeAt(j);
if (DEBUG) {
Slog.v(TAG, "Removing " + approvedPackageOrComponent
- + " from approved list; no bind permission found "
+ + " from approved list; no bind permission or "
+ + "service interface filter found "
+ mConfig.bindPermission);
}
}
@@ -1325,6 +1326,11 @@
}
}
+ protected boolean isValidService(ComponentName component, int userId) {
+ return componentHasBindPermission(component, userId) && queryPackageForServices(
+ component.getPackageName(), userId).contains(component);
+ }
+
protected boolean isValidEntry(String packageOrComponent, int userId) {
return hasMatchingServices(packageOrComponent, userId);
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 7534bfe..d0706d2 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1158,8 +1158,7 @@
break;
case SHORT_PRESS_POWER_CLOSE_IME_OR_GO_HOME: {
if (mDismissImeOnBackKeyPressed) {
- // TODO(b/308479256): Check if hiding "all" IMEs is OK or not.
- InputMethodManagerInternal.get().hideAllInputMethods(
+ InputMethodManagerInternal.get().hideInputMethod(
SoftInputShowHideReason.HIDE_POWER_BUTTON_GO_HOME, displayId);
} else {
shortPressPowerGoHome();
diff --git a/services/core/java/com/android/server/power/hint/TEST_MAPPING b/services/core/java/com/android/server/power/hint/TEST_MAPPING
index 9988786..5450700 100644
--- a/services/core/java/com/android/server/power/hint/TEST_MAPPING
+++ b/services/core/java/com/android/server/power/hint/TEST_MAPPING
@@ -1,5 +1,5 @@
{
- "postsubmit": [
+ "presubmit": [
{
"name": "PerformanceHintTests",
"options": [
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 85c8900..e9423ce 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -1890,8 +1890,7 @@
enforceStatusBarService();
final long token = Binder.clearCallingIdentity();
try {
- // TODO(b/308479256): Check if hiding "all" IMEs is OK or not.
- InputMethodManagerInternal.get().hideAllInputMethods(
+ InputMethodManagerInternal.get().hideInputMethod(
SoftInputShowHideReason.HIDE_BUBBLES, displayId);
} finally {
Binder.restoreCallingIdentity(token);
diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java
index f9bad59..46bd7af 100644
--- a/services/core/java/com/android/server/vibrator/HalVibration.java
+++ b/services/core/java/com/android/server/vibrator/HalVibration.java
@@ -108,23 +108,6 @@
}
/**
- * Resolves the default vibration amplitude of {@link #getEffectToPlay()} and each fallback.
- *
- * @param defaultAmplitude An integer in [1,255] representing the device default amplitude to
- * replace the {@link VibrationEffect#DEFAULT_AMPLITUDE}.
- */
- public void resolveEffects(int defaultAmplitude) {
- CombinedVibration newEffect =
- mEffectToPlay.transform(VibrationEffect::resolve, defaultAmplitude);
- if (!Objects.equals(mEffectToPlay, newEffect)) {
- mEffectToPlay = newEffect;
- }
- for (int i = 0; i < mFallbacks.size(); i++) {
- mFallbacks.setValueAt(i, mFallbacks.valueAt(i).resolve(defaultAmplitude));
- }
- }
-
- /**
* Scales the {@link #getEffectToPlay()} and each fallback effect based on the vibration usage.
*/
public void scaleEffects(VibrationScaler scaler) {
diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
index 98a2ba0d..3f9da82 100644
--- a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
+++ b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
@@ -46,6 +46,8 @@
VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
private static final VibrationAttributes COMMUNICATION_REQUEST_VIBRATION_ATTRIBUTES =
VibrationAttributes.createForUsage(VibrationAttributes.USAGE_COMMUNICATION_REQUEST);
+ private static final VibrationAttributes IME_FEEDBACK_VIBRATION_ATTRIBUTES =
+ VibrationAttributes.createForUsage(VibrationAttributes.USAGE_IME_FEEDBACK);
private final VibratorInfo mVibratorInfo;
private final boolean mHapticTextHandleEnabled;
@@ -219,8 +221,6 @@
}
int vibFlags = 0;
- boolean fromIme =
- (privFlags & HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS) != 0;
boolean bypassVibrationIntensitySetting =
(flags & HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0;
if (bypassVibrationIntensitySetting) {
@@ -229,9 +229,6 @@
if (shouldBypassInterruptionPolicy(effectId)) {
vibFlags |= VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY;
}
- if (shouldBypassIntensityScale(effectId, fromIme)) {
- vibFlags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE;
- }
return vibFlags == 0 ? attrs : new VibrationAttributes.Builder(attrs)
.setFlags(vibFlags).build();
@@ -362,22 +359,6 @@
/* fallbackForPredefinedEffect= */ predefinedEffectFallback);
}
- private boolean shouldBypassIntensityScale(int effectId, boolean isIme) {
- if (!Flags.keyboardCategoryEnabled() || mKeyboardVibrationFixedAmplitude < 0 || !isIme) {
- // Shouldn't bypass if not support keyboard category, no fixed amplitude or not an IME.
- return false;
- }
- switch (effectId) {
- case HapticFeedbackConstants.KEYBOARD_TAP:
- return mVibratorInfo.isPrimitiveSupported(
- VibrationEffect.Composition.PRIMITIVE_CLICK);
- case HapticFeedbackConstants.KEYBOARD_RELEASE:
- return mVibratorInfo.isPrimitiveSupported(
- VibrationEffect.Composition.PRIMITIVE_TICK);
- }
- return false;
- }
-
private VibrationAttributes createKeyboardVibrationAttributes(
@HapticFeedbackConstants.PrivateFlags int privFlags) {
// Use touch attribute when the keyboard category is disable.
@@ -388,7 +369,8 @@
if ((privFlags & HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS) == 0) {
return TOUCH_VIBRATION_ATTRIBUTES;
}
- return new VibrationAttributes.Builder(TOUCH_VIBRATION_ATTRIBUTES)
+ return new VibrationAttributes.Builder(IME_FEEDBACK_VIBRATION_ATTRIBUTES)
+ // TODO(b/332661766): Remove CATEGORY_KEYBOARD logic
.setCategory(VibrationAttributes.CATEGORY_KEYBOARD)
.build();
}
diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java
index 0206155..fb92d60 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSettings.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java
@@ -21,6 +21,7 @@
import static android.os.VibrationAttributes.USAGE_ALARM;
import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK;
+import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK;
import static android.os.VibrationAttributes.USAGE_MEDIA;
import static android.os.VibrationAttributes.USAGE_NOTIFICATION;
import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION;
@@ -560,6 +561,7 @@
mKeyboardVibrationOn = loadSystemSetting(
Settings.System.KEYBOARD_VIBRATION_ENABLED, 1, userHandle) > 0;
+ int keyboardIntensity = getDefaultIntensity(USAGE_IME_FEEDBACK);
int alarmIntensity = toIntensity(
loadSystemSetting(Settings.System.ALARM_VIBRATION_INTENSITY, -1, userHandle),
getDefaultIntensity(USAGE_ALARM));
@@ -610,6 +612,12 @@
mCurrentVibrationIntensities.put(USAGE_TOUCH, hapticFeedbackIntensity);
}
+ if (mVibrationConfig.isKeyboardVibrationSettingsSupported()) {
+ mCurrentVibrationIntensities.put(USAGE_IME_FEEDBACK, keyboardIntensity);
+ } else {
+ mCurrentVibrationIntensities.put(USAGE_IME_FEEDBACK, hapticFeedbackIntensity);
+ }
+
// A11y is not disabled by any haptic feedback setting.
mCurrentVibrationIntensities.put(USAGE_ACCESSIBILITY, positiveHapticFeedbackIntensity);
}
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 8c9a92d..7152844 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -21,7 +21,6 @@
import android.os.Build;
import android.os.CombinedVibration;
import android.os.IBinder;
-import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
@@ -177,16 +176,11 @@
expectIsVibrationThread(true);
}
- if (!mVibration.callerInfo.attrs.isFlagSet(
- VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)) {
- if (Flags.adaptiveHapticsEnabled()) {
- waitForVibrationParamsIfRequired();
- }
- // Scale resolves the default amplitudes from the effect before scaling them.
- mVibration.scaleEffects(mVibrationScaler);
- } else {
- mVibration.resolveEffects(mVibrationScaler.getDefaultVibrationAmplitude());
+ if (Flags.adaptiveHapticsEnabled()) {
+ waitForVibrationParamsIfRequired();
}
+ // Scale resolves the default amplitudes from the effect before scaling them.
+ mVibration.scaleEffects(mVibrationScaler);
mVibration.adaptToDevice(mDeviceAdapter);
CombinedVibration.Sequential sequentialEffect = toSequential(mVibration.getEffectToPlay());
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 48c4a68..7610d7d 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -103,8 +103,7 @@
new VibrationAttributes.Builder().build();
private static final int ATTRIBUTES_ALL_BYPASS_FLAGS =
VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY
- | VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF
- | VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE;
+ | VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF;
/** Fixed large duration used to note repeating vibrations to {@link IBatteryStats}. */
private static final long BATTERY_STATS_REPEATING_VIBRATION_DURATION = 5_000;
@@ -925,8 +924,7 @@
private VibrationStepConductor createVibrationStepConductor(HalVibration vib) {
CompletableFuture<Void> requestVibrationParamsFuture = null;
- if (Flags.adaptiveHapticsEnabled() && !vib.callerInfo.attrs.isFlagSet(
- VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)
+ if (Flags.adaptiveHapticsEnabled()
&& mVibratorControlService.shouldRequestVibrationParams(
vib.callerInfo.attrs.getUsage())) {
requestVibrationParamsFuture =
@@ -940,13 +938,8 @@
}
private Vibration.EndInfo startVibrationOnInputDevicesLocked(HalVibration vib) {
- if (!vib.callerInfo.attrs.isFlagSet(
- VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)) {
- // Scale resolves the default amplitudes from the effect before scaling them.
- vib.scaleEffects(mVibrationScaler);
- } else {
- vib.resolveEffects(mVibrationScaler.getDefaultVibrationAmplitude());
- }
+ // Scale resolves the default amplitudes from the effect before scaling them.
+ vib.scaleEffects(mVibrationScaler);
mInputDeviceDelegate.vibrateIfAvailable(vib.callerInfo, vib.getEffectToPlay());
return new Vibration.EndInfo(Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index f70a3ba..d5bea4a 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -676,7 +676,12 @@
final Rect estimateCrop = new Rect(cropHint);
if (!multiCrop()) estimateCrop.scale(1f / options.inSampleSize);
- else estimateCrop.scale(1f / sampleSize);
+ else {
+ estimateCrop.left = (int) Math.floor(estimateCrop.left / sampleSize);
+ estimateCrop.top = (int) Math.floor(estimateCrop.top / sampleSize);
+ estimateCrop.right = (int) Math.ceil(estimateCrop.right / sampleSize);
+ estimateCrop.bottom = (int) Math.ceil(estimateCrop.bottom / sampleSize);
+ }
float hRatio = (float) wpData.mHeight / estimateCrop.height();
final int destHeight = (int) (estimateCrop.height() * hRatio);
final int destWidth = (int) (estimateCrop.width() * hRatio);
@@ -720,7 +725,10 @@
}
if (multiCrop()) {
Slog.v(TAG, " cropHint=" + cropHint);
+ Slog.v(TAG, " estimateCrop=" + estimateCrop);
Slog.v(TAG, " sampleSize=" + sampleSize);
+ Slog.v(TAG, " user defined crops: " + wallpaper.mCropHints);
+ Slog.v(TAG, " all crops: " + defaultCrops);
}
Slog.v(TAG, " targetSize=" + safeWidth + "x" + safeHeight);
Slog.v(TAG, " maxTextureSize=" + GLHelper.getMaxTextureSize());
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index 3b99954..1994174 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -52,7 +52,7 @@
* display change transition. In this case, we will queue all display updates until the current
* transition's collection finishes and then apply them afterwards.
*/
-public class DeferredDisplayUpdater implements DisplayUpdater {
+class DeferredDisplayUpdater {
/**
* List of fields that could be deferred before applying to DisplayContent.
@@ -110,7 +110,7 @@
continueScreenUnblocking();
};
- public DeferredDisplayUpdater(@NonNull DisplayContent displayContent) {
+ DeferredDisplayUpdater(@NonNull DisplayContent displayContent) {
mDisplayContent = displayContent;
mNonOverrideDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
}
@@ -122,8 +122,7 @@
*
* @param finishCallback is called when all pending display updates are finished
*/
- @Override
- public void updateDisplayInfo(@NonNull Runnable finishCallback) {
+ void updateDisplayInfo(@NonNull Runnable finishCallback) {
// Get the latest display parameters from the DisplayManager
final DisplayInfo displayInfo = getCurrentDisplayInfo();
@@ -310,9 +309,11 @@
return !Objects.equals(first.uniqueId, second.uniqueId);
}
- @Override
- public void onDisplayContentDisplayPropertiesPostChanged(int previousRotation, int newRotation,
- DisplayAreaInfo newDisplayAreaInfo) {
+ /**
+ * Called after physical display has changed and after DisplayContent applied new display
+ * properties.
+ */
+ void onDisplayContentDisplayPropertiesPostChanged() {
// Unblock immediately in case there is no transition. This is unlikely to happen.
if (mScreenUnblocker != null && !mDisplayContent.mTransitionController.inTransition()) {
mScreenUnblocker.sendToTarget();
@@ -320,13 +321,16 @@
}
}
- @Override
- public void onDisplaySwitching(boolean switching) {
+ /**
+ * Called with {@code true} when physical display is going to switch. And {@code false} when
+ * the display is turned on or the device goes to sleep.
+ */
+ void onDisplaySwitching(boolean switching) {
mShouldWaitForTransitionWhenScreenOn = switching;
}
- @Override
- public boolean waitForTransition(@NonNull Message screenUnblocker) {
+ /** Returns {@code true} if the transition will control when to turn on the screen. */
+ boolean waitForTransition(@NonNull Message screenUnblocker) {
if (!Flags.waitForTransitionOnDisplaySwitch()) return false;
if (!mShouldWaitForTransitionWhenScreenOn) {
return false;
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 403c307..b31ae90 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -159,7 +159,6 @@
import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields;
import static com.android.server.wm.utils.RegionUtils.forEachRectReverse;
import static com.android.server.wm.utils.RegionUtils.rectListToRegion;
-import static com.android.window.flags.Flags.deferDisplayUpdates;
import static com.android.window.flags.Flags.explicitRefreshRateHints;
import android.annotation.IntDef;
@@ -478,7 +477,7 @@
AppCompatCameraPolicy mAppCompatCameraPolicy;
DisplayFrames mDisplayFrames;
- final DisplayUpdater mDisplayUpdater;
+ final DeferredDisplayUpdater mDisplayUpdater;
private boolean mInTouchMode;
@@ -623,7 +622,6 @@
@VisibleForTesting
final DeviceStateController mDeviceStateController;
final Consumer<DeviceStateController.DeviceState> mDeviceStateConsumer;
- final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher;
final RemoteDisplayChangeController mRemoteDisplayChangeController;
/** Windows added since {@link #mCurrentFocus} was set to null. Used for ANR blaming. */
@@ -1140,11 +1138,7 @@
mWallpaperController.resetLargestDisplay(display);
display.getDisplayInfo(mDisplayInfo);
display.getMetrics(mDisplayMetrics);
- if (deferDisplayUpdates()) {
- mDisplayUpdater = new DeferredDisplayUpdater(this);
- } else {
- mDisplayUpdater = new ImmediateDisplayUpdater(this);
- }
+ mDisplayUpdater = new DeferredDisplayUpdater(this);
mSystemGestureExclusionLimit = mWmService.mConstants.mSystemGestureExclusionLimitDp
* mDisplayMetrics.densityDpi / DENSITY_DEFAULT;
isDefaultDisplay = mDisplayId == DEFAULT_DISPLAY;
@@ -1168,8 +1162,6 @@
mAppTransitionController = new AppTransitionController(mWmService, this);
mTransitionController.registerLegacyListener(mFixedRotationTransitionListener);
mUnknownAppVisibilityController = new UnknownAppVisibilityController(mWmService, this);
- mDisplaySwitchTransitionLauncher = new PhysicalDisplaySwitchTransitionLauncher(this,
- mTransitionController);
mRemoteDisplayChangeController = new RemoteDisplayChangeController(this);
final InputChannel inputChannel = mWmService.mInputManager.monitorInput(
@@ -1190,7 +1182,6 @@
mDeviceStateConsumer =
(@NonNull DeviceStateController.DeviceState newFoldState) -> {
- mDisplaySwitchTransitionLauncher.foldStateChanged(newFoldState);
mDisplayRotation.foldStateChanged(newFoldState);
};
mDeviceStateController.registerDeviceStateCallback(mDeviceStateConsumer,
@@ -3094,8 +3085,6 @@
// metrics are updated as rotation settings might depend on them
mWmService.mDisplayWindowSettings.applySettingsToDisplayLocked(this,
/* includeRotationSettings */ false);
- mDisplayUpdater.onDisplayContentDisplayPropertiesPreChanged(mDisplayId,
- mInitialDisplayWidth, mInitialDisplayHeight, newWidth, newHeight);
mDisplayRotation.physicalDisplayChanged();
mDisplayPolicy.physicalDisplayChanged();
}
@@ -3130,8 +3119,7 @@
if (physicalDisplayChanged) {
mDisplayPolicy.physicalDisplayUpdated();
- mDisplayUpdater.onDisplayContentDisplayPropertiesPostChanged(currentRotation,
- getRotation(), getDisplayAreaInfo());
+ mDisplayUpdater.onDisplayContentDisplayPropertiesPostChanged();
}
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayUpdater.java b/services/core/java/com/android/server/wm/DisplayUpdater.java
deleted file mode 100644
index 918b180..0000000
--- a/services/core/java/com/android/server/wm/DisplayUpdater.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import android.annotation.NonNull;
-import android.os.Message;
-import android.view.Surface;
-import android.window.DisplayAreaInfo;
-
-/**
- * Interface for a helper class that manages updates of DisplayInfo coming from DisplayManager
- */
-interface DisplayUpdater {
- /**
- * Reads the latest display parameters from the display manager and returns them in a callback.
- * If there are pending display updates, it will wait for them to finish first and only then it
- * will call the callback with the latest display parameters.
- *
- * @param callback is called when all pending display updates are finished
- */
- void updateDisplayInfo(@NonNull Runnable callback);
-
- /**
- * Called when physical display has changed and before DisplayContent has applied new display
- * properties
- */
- default void onDisplayContentDisplayPropertiesPreChanged(int displayId, int initialDisplayWidth,
- int initialDisplayHeight, int newWidth, int newHeight) {
- }
-
- /**
- * Called after physical display has changed and after DisplayContent applied new display
- * properties
- */
- default void onDisplayContentDisplayPropertiesPostChanged(
- @Surface.Rotation int previousRotation, @Surface.Rotation int newRotation,
- @NonNull DisplayAreaInfo newDisplayAreaInfo) {
- }
-
- /**
- * Called with {@code true} when physical display is going to switch. And {@code false} when
- * the display is turned on or the device goes to sleep.
- */
- default void onDisplaySwitching(boolean switching) {
- }
-
- /** Returns {@code true} if the transition will control when to turn on the screen. */
- default boolean waitForTransition(@NonNull Message screenUnBlocker) {
- return false;
- }
-}
diff --git a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
index e0d69b0..4ec318b 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
@@ -16,9 +16,12 @@
package com.android.server.wm;
+import static android.content.pm.ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.WindowConfiguration;
+import android.companion.virtualdevice.flags.Flags;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -26,6 +29,7 @@
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Slog;
+import android.view.Display;
import android.window.DisplayWindowPolicyController;
import java.io.PrintWriter;
@@ -80,6 +84,9 @@
if (hasDisplayCategory(activities.get(i))) {
return false;
}
+ if (!launchAllowedByDisplayPolicy(activities.get(i))) {
+ return false;
+ }
}
return true;
}
@@ -95,7 +102,13 @@
if (mDisplayWindowPolicyController == null) {
// Missing controller means that this display has no categories for activity launch
// restriction.
- return !hasDisplayCategory(activityInfo);
+ if (hasDisplayCategory(activityInfo)) {
+ return false;
+ }
+ if (!launchAllowedByDisplayPolicy(activityInfo)) {
+ return false;
+ }
+ return true;
}
return mDisplayWindowPolicyController.canActivityBeLaunched(activityInfo, intent,
windowingMode, launchingFromDisplayId, isNewTask);
@@ -112,6 +125,24 @@
return false;
}
+ private boolean launchAllowedByDisplayPolicy(ActivityInfo aInfo) {
+ if (!Flags.enforceRemoteDeviceOptOutOnAllVirtualDisplays()) {
+ return true;
+ }
+ int displayType = mDisplayContent.getDisplay().getType();
+ if (displayType != Display.TYPE_VIRTUAL && displayType != Display.TYPE_WIFI) {
+ return true;
+ }
+ if ((aInfo.flags & FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES) == 0) {
+ Slog.d(TAG,
+ String.format("Checking activity launch on display %d, activity requires"
+ + " android:canDisplayOnRemoteDevices=true",
+ mDisplayContent.mDisplayId));
+ return false;
+ }
+ return true;
+ }
+
/**
* @see DisplayWindowPolicyController#keepActivityOnWindowFlagsChanged(ActivityInfo, int, int)
*/
diff --git a/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java b/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java
deleted file mode 100644
index 4af9013..0000000
--- a/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import android.annotation.NonNull;
-import android.view.DisplayInfo;
-import android.window.DisplayAreaInfo;
-
-/**
- * DisplayUpdater that immediately applies new DisplayInfo properties
- */
-public class ImmediateDisplayUpdater implements DisplayUpdater {
-
- private final DisplayContent mDisplayContent;
- private final DisplayInfo mDisplayInfo = new DisplayInfo();
-
- public ImmediateDisplayUpdater(@NonNull DisplayContent displayContent) {
- mDisplayContent = displayContent;
- mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
- }
-
- @Override
- public void updateDisplayInfo(Runnable callback) {
- mDisplayContent.mWmService.mDisplayManagerInternal.getNonOverrideDisplayInfo(
- mDisplayContent.mDisplayId, mDisplayInfo);
- mDisplayContent.onDisplayInfoUpdated(mDisplayInfo);
- callback.run();
- }
-
- @Override
- public void onDisplayContentDisplayPropertiesPreChanged(int displayId, int initialDisplayWidth,
- int initialDisplayHeight, int newWidth, int newHeight) {
- mDisplayContent.mDisplaySwitchTransitionLauncher.requestDisplaySwitchTransitionIfNeeded(
- displayId, initialDisplayWidth, initialDisplayHeight, newWidth, newHeight);
- }
-
- @Override
- public void onDisplayContentDisplayPropertiesPostChanged(int previousRotation, int newRotation,
- DisplayAreaInfo newDisplayAreaInfo) {
- mDisplayContent.mDisplaySwitchTransitionLauncher.onDisplayUpdated(previousRotation,
- newRotation,
- newDisplayAreaInfo);
- }
-}
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index b496a65..b8869f1 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -439,8 +439,7 @@
final InputMethodManagerInternal inputMethodManagerInternal =
LocalServices.getService(InputMethodManagerInternal.class);
if (inputMethodManagerInternal != null) {
- // TODO(b/308479256): Check if hiding "all" IMEs is OK or not.
- inputMethodManagerInternal.hideAllInputMethods(
+ inputMethodManagerInternal.hideInputMethod(
SoftInputShowHideReason.HIDE_RECENTS_ANIMATION,
mDisplayContent.getDisplayId());
}
diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
deleted file mode 100644
index 3cf301c..0000000
--- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH;
-
-import static com.android.internal.R.bool.config_unfoldTransitionEnabled;
-import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;
-import static com.android.server.wm.DeviceStateController.DeviceState.FOLDED;
-import static com.android.server.wm.DeviceStateController.DeviceState.HALF_FOLDED;
-import static com.android.server.wm.DeviceStateController.DeviceState.OPEN;
-
-import android.animation.ValueAnimator;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.graphics.Rect;
-import android.window.DisplayAreaInfo;
-import android.window.TransitionRequestInfo;
-import android.window.WindowContainerTransaction;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLogGroup;
-import com.android.internal.protolog.ProtoLog;
-import com.android.server.wm.DeviceStateController.DeviceState;
-
-public class PhysicalDisplaySwitchTransitionLauncher {
-
- private final DisplayContent mDisplayContent;
- private final ActivityTaskManagerService mAtmService;
- private final Context mContext;
- private final TransitionController mTransitionController;
-
- /**
- * If on a foldable device represents whether we need to show unfold animation when receiving
- * a physical display switch event
- */
- private boolean mShouldRequestTransitionOnDisplaySwitch = false;
- /**
- * Current device state from {@link android.hardware.devicestate.DeviceStateManager}
- */
- private DeviceState mDeviceState = DeviceState.UNKNOWN;
- private Transition mTransition;
-
- public PhysicalDisplaySwitchTransitionLauncher(DisplayContent displayContent,
- TransitionController transitionController) {
- this(displayContent, displayContent.mWmService.mAtmService,
- displayContent.mWmService.mContext, transitionController);
- }
-
- @VisibleForTesting
- public PhysicalDisplaySwitchTransitionLauncher(DisplayContent displayContent,
- ActivityTaskManagerService service, Context context,
- TransitionController transitionController) {
- mDisplayContent = displayContent;
- mAtmService = service;
- mContext = context;
- mTransitionController = transitionController;
- }
-
- /**
- * Called by the display manager just before it applied the device state, it is guaranteed
- * that in case of physical display change the
- * {@link PhysicalDisplaySwitchTransitionLauncher#requestDisplaySwitchTransitionIfNeeded}
- * method will be invoked *after* this one.
- */
- void foldStateChanged(DeviceState newDeviceState) {
- boolean isUnfolding = mDeviceState == FOLDED
- && (newDeviceState == HALF_FOLDED || newDeviceState == OPEN);
-
- if (isUnfolding) {
- // Request transition only if we are unfolding the device
- mShouldRequestTransitionOnDisplaySwitch = true;
- } else if (newDeviceState != HALF_FOLDED && newDeviceState != OPEN) {
- // Cancel the transition request in case if we are folding or switching to back
- // to the rear display before the displays got switched
- mShouldRequestTransitionOnDisplaySwitch = false;
- }
-
- mDeviceState = newDeviceState;
- }
-
- /**
- * Requests to start a transition for the physical display switch
- */
- public void requestDisplaySwitchTransitionIfNeeded(int displayId, int oldDisplayWidth,
- int oldDisplayHeight, int newDisplayWidth, int newDisplayHeight) {
- if (!mShouldRequestTransitionOnDisplaySwitch) return;
- if (!mTransitionController.isShellTransitionsEnabled()) return;
- if (!mDisplayContent.getLastHasContent()) return;
-
- boolean shouldRequestUnfoldTransition = mContext.getResources()
- .getBoolean(config_unfoldTransitionEnabled) && ValueAnimator.areAnimatorsEnabled();
-
- if (!shouldRequestUnfoldTransition) {
- return;
- }
-
- mTransition = null;
-
- if (mTransitionController.isCollecting()) {
- // Add display container to the currently collecting transition
- mTransition = mTransitionController.getCollectingTransition();
- mTransition.collect(mDisplayContent);
-
- // Make sure that transition is not ready until we finish the remote display change
- mTransition.setReady(mDisplayContent, false);
- mTransition.addFlag(TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH);
-
- ProtoLog.d(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
- "Adding display switch to existing collecting transition");
- } else {
- final TransitionRequestInfo.DisplayChange displayChange =
- new TransitionRequestInfo.DisplayChange(displayId);
-
- final Rect startAbsBounds = new Rect(0, 0, oldDisplayWidth, oldDisplayHeight);
- displayChange.setStartAbsBounds(startAbsBounds);
- final Rect endAbsBounds = new Rect(0, 0, newDisplayWidth, newDisplayHeight);
- displayChange.setEndAbsBounds(endAbsBounds);
- displayChange.setPhysicalDisplayChanged(true);
-
- mTransition = mTransitionController.requestStartDisplayTransition(TRANSIT_CHANGE,
- 0 /* flags */, mDisplayContent, null /* remoteTransition */, displayChange);
- mTransition.collect(mDisplayContent);
- }
-
- if (mTransition != null) {
- mAtmService.startPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY);
- }
-
- mShouldRequestTransitionOnDisplaySwitch = false;
- }
-
- /**
- * Called when physical display is getting updated, this could happen e.g. on foldable
- * devices when the physical underlying display is replaced.
- *
- * @param fromRotation rotation before the display change
- * @param toRotation rotation after the display change
- * @param newDisplayAreaInfo display area info after the display change
- */
- public void onDisplayUpdated(int fromRotation, int toRotation,
- @NonNull DisplayAreaInfo newDisplayAreaInfo) {
- if (mTransition == null) return;
-
- final boolean started = mDisplayContent.mRemoteDisplayChangeController
- .performRemoteDisplayChange(fromRotation, toRotation, newDisplayAreaInfo,
- this::continueDisplayUpdate);
-
- if (!started) {
- markTransitionAsReady();
- }
- }
-
- private void continueDisplayUpdate(@Nullable WindowContainerTransaction transaction) {
- if (mTransition == null) return;
-
- if (transaction != null) {
- mAtmService.mWindowOrganizerController.applyTransaction(transaction);
- }
-
- markTransitionAsReady();
- }
-
- private void markTransitionAsReady() {
- if (mTransition == null) return;
-
- mTransition.setAllReady();
- mTransition = null;
- }
-
-}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index efcc23f..8ae4f9a 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -41,6 +41,7 @@
import static java.util.Objects.requireNonNull;
+import android.annotation.Nullable;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
@@ -50,6 +51,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.inputmethod.InputBindResult;
import com.android.internal.inputmethod.SoftInputShowHideReason;
import com.android.internal.inputmethod.StartInputFlags;
@@ -75,7 +77,7 @@
super.setUp();
synchronized (ImfLock.class) {
mVisibilityApplier = mInputMethodManagerService.getVisibilityApplierLocked();
- mInputMethodManagerService.setAttachedClientForTesting(requireNonNull(
+ setAttachedClientLocked(requireNonNull(
mInputMethodManagerService.getClientStateLocked(mMockInputMethodClient)));
}
}
@@ -168,7 +170,9 @@
// Init a IME target client on the secondary display to show IME.
mInputMethodManagerService.addClient(mMockInputMethodClient, mMockRemoteInputConnection,
10 /* selfReportedDisplayId */);
- mInputMethodManagerService.setAttachedClientForTesting(null);
+ synchronized (ImfLock.class) {
+ setAttachedClientLocked(null);
+ }
startInputOrWindowGainedFocus(mWindowToken, SOFT_INPUT_STATE_ALWAYS_VISIBLE);
final var statsToken = ImeTracker.Token.empty();
@@ -206,7 +210,9 @@
@Test
public void testApplyImeVisibility_hideImeWhenUnbinding() {
- mInputMethodManagerService.setAttachedClientForTesting(null);
+ synchronized (ImfLock.class) {
+ setAttachedClientLocked(null);
+ }
startInputOrWindowGainedFocus(mWindowToken, SOFT_INPUT_STATE_ALWAYS_VISIBLE);
ExtendedMockito.spyOn(mVisibilityApplier);
@@ -233,6 +239,11 @@
}
}
+ @GuardedBy("ImfLock.class")
+ private void setAttachedClientLocked(@Nullable ClientState cs) {
+ mInputMethodManagerService.getUserData(mUserId).mCurClient = cs;
+ }
+
private InputBindResult startInputOrWindowGainedFocus(IBinder windowToken, int softInputMode) {
return mInputMethodManagerService.startInputOrWindowGainedFocus(
StartInputReason.WINDOW_FOCUS_GAIN /* startInputReason */,
diff --git a/services/tests/performancehinttests/TEST_MAPPING b/services/tests/performancehinttests/TEST_MAPPING
index faffe35..fa7b897 100644
--- a/services/tests/performancehinttests/TEST_MAPPING
+++ b/services/tests/performancehinttests/TEST_MAPPING
@@ -1,4 +1,12 @@
{
+ "presubmit": [
+ {
+ "name": "PerformanceHintTests",
+ "options": [
+ {"exclude-annotation": "org.junit.Ignore"}
+ ]
+ }
+ ],
"ravenwood-postsubmit": [
{
"name": "PerformanceHintTestsRavenwood",
@@ -7,13 +15,5 @@
{"exclude-annotation": "android.platform.test.annotations.DisabledOnRavenwood"}
]
}
- ],
- "postsubmit": [
- {
- "name": "PerformanceHintTests",
- "options": [
- {"exclude-annotation": "org.junit.Ignore"}
- ]
- }
]
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 20b9592..1afe12f 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -840,6 +840,10 @@
info_a.setComponentName(COMPONENT_NAME);
final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo();
info_b.setComponentName(new ComponentName("package", "class"));
+ writeStringsToSetting(Set.of(
+ info_a.getComponentName().flattenToString(),
+ info_b.getComponentName().flattenToString()),
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
AccessibilityUserState userState = mA11yms.getCurrentUserState();
userState.mInstalledServices.clear();
@@ -858,10 +862,9 @@
userState = mA11yms.getCurrentUserState();
assertThat(userState.mEnabledServices).containsExactly(info_b.getComponentName());
//Assert setting change
- final Set<ComponentName> componentsFromSetting = new ArraySet<>();
- mA11yms.readComponentNamesFromSettingLocked(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
- userState.mUserId, componentsFromSetting);
- assertThat(componentsFromSetting).containsExactly(info_b.getComponentName());
+ final Set<String> enabledServices =
+ readStringsFromSetting(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
+ assertThat(enabledServices).containsExactly(info_b.getComponentName().flattenToString());
}
@Test
@@ -880,6 +883,10 @@
info_a.getComponentName().flattenToString(),
info_b.getComponentName().flattenToString()),
SOFTWARE);
+ writeStringsToSetting(Set.of(
+ info_a.getComponentName().flattenToString(),
+ info_b.getComponentName().flattenToString()),
+ ShortcutUtils.convertToKey(SOFTWARE));
// despite force stopping both packages, only the first service has the relevant flag,
// so only the first should be removed.
@@ -896,13 +903,53 @@
assertThat(userState.getShortcutTargetsLocked(SOFTWARE)).containsExactly(
info_b.getComponentName().flattenToString());
//Assert setting change
- final Set<String> targetsFromSetting = new ArraySet<>();
- mA11yms.readColonDelimitedSettingToSet(ShortcutUtils.convertToKey(SOFTWARE),
- userState.mUserId, str -> str, targetsFromSetting);
+ final Set<String> targetsFromSetting = readStringsFromSetting(
+ ShortcutUtils.convertToKey(SOFTWARE));
assertThat(targetsFromSetting).containsExactly(info_b.getComponentName().flattenToString());
}
@Test
+ public void testPackagesForceStopped_otherServiceStopped_doesNotRemoveContinuousTarget() {
+ final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo();
+ info_a.setComponentName(COMPONENT_NAME);
+ info_a.flags = FLAG_REQUEST_ACCESSIBILITY_BUTTON;
+ final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo();
+ info_b.setComponentName(new ComponentName("package", "class"));
+ writeStringsToSetting(Set.of(
+ info_a.getComponentName().flattenToString(),
+ info_b.getComponentName().flattenToString()),
+ ShortcutUtils.convertToKey(SOFTWARE));
+
+ AccessibilityUserState userState = mA11yms.getCurrentUserState();
+ userState.mInstalledServices.clear();
+ userState.mInstalledServices.add(info_a);
+ userState.mInstalledServices.add(info_b);
+ userState.updateShortcutTargetsLocked(Set.of(
+ info_a.getComponentName().flattenToString(),
+ info_b.getComponentName().flattenToString()),
+ SOFTWARE);
+
+ // Force stopping a service should not disable unrelated continuous services.
+ synchronized (mA11yms.getLock()) {
+ mA11yms.onPackagesForceStoppedLocked(
+ new String[]{info_b.getComponentName().getPackageName()},
+ userState);
+ }
+
+ //Assert user state change
+ userState = mA11yms.getCurrentUserState();
+ assertThat(userState.getShortcutTargetsLocked(SOFTWARE)).containsExactly(
+ info_a.getComponentName().flattenToString(),
+ info_b.getComponentName().flattenToString());
+ //Assert setting unchanged
+ final Set<String> targetsFromSetting = readStringsFromSetting(
+ ShortcutUtils.convertToKey(SOFTWARE));
+ assertThat(targetsFromSetting).containsExactly(
+ info_a.getComponentName().flattenToString(),
+ info_b.getComponentName().flattenToString());
+ }
+
+ @Test
public void testPackageMonitorScanPackages_scansWithoutHoldingLock() {
setupAccessibilityServiceConnection(0);
final AtomicReference<Set<Boolean>> lockState = collectLockStateWhilePackageScanning();
@@ -1844,6 +1891,11 @@
return result;
}
+ private void writeStringsToSetting(Set<String> strings, String setting) {
+ mA11yms.persistColonDelimitedSetToSettingLocked(
+ setting, UserHandle.USER_SYSTEM, strings, str -> str);
+ }
+
private void broadcastSettingRestored(String setting, String newValue) {
Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java
new file mode 100644
index 0000000..b5a538f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.audio;
+
+import static com.android.media.audio.Flags.asDeviceConnectionFailure;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.platform.test.annotations.Presubmit;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioDeviceInventoryTest {
+
+ private static final String TAG = "AudioDeviceInventoryTest";
+
+ @Mock private AudioService mMockAudioService;
+ private AudioDeviceInventory mDevInventory;
+ @Spy private AudioDeviceBroker mSpyAudioDeviceBroker;
+ @Spy private AudioSystemAdapter mSpyAudioSystem;
+
+ private SystemServerAdapter mSystemServer;
+
+ private BluetoothDevice mFakeBtDevice;
+
+ @Before
+ public void setUp() throws Exception {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ mMockAudioService = mock(AudioService.class);
+ mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
+ mDevInventory = new AudioDeviceInventory(mSpyAudioSystem);
+ mSystemServer = new NoOpSystemServerAdapter();
+ mSpyAudioDeviceBroker = spy(new AudioDeviceBroker(context, mMockAudioService, mDevInventory,
+ mSystemServer, mSpyAudioSystem));
+ mDevInventory.setDeviceBroker(mSpyAudioDeviceBroker);
+
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ mFakeBtDevice = adapter.getRemoteDevice("00:01:02:03:04:05");
+ }
+
+ @After
+ public void tearDown() throws Exception { }
+
+ /**
+ * test that for DEVICE_OUT_BLUETOOTH_A2DP devices, when the device connects, it's only
+ * added to the connected devices when the connection through AudioSystem is successful
+ * @throws Exception on error
+ */
+ @Test
+ public void testSetDeviceConnectionStateA2dp() throws Exception {
+ Log.i(TAG, "starting testSetDeviceConnectionStateA2dp");
+ assertTrue("collection of connected devices not empty at start",
+ mDevInventory.getConnectedDevices().isEmpty());
+
+ final AudioDeviceAttributes ada = new AudioDeviceAttributes(
+ AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, mFakeBtDevice.getAddress());
+ AudioDeviceBroker.BtDeviceInfo btInfo =
+ new AudioDeviceBroker.BtDeviceInfo(mFakeBtDevice, BluetoothProfile.A2DP,
+ BluetoothProfile.STATE_CONNECTED, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
+ AudioSystem.AUDIO_FORMAT_SBC);
+
+ // test that no device is added when AudioSystem returns AUDIO_STATUS_ERROR
+ // when setDeviceConnectionState is called for the connection
+ // NOTE: for now this is only when flag asDeviceConnectionFailure is true
+ if (asDeviceConnectionFailure()) {
+ when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE,
+ AudioSystem.AUDIO_FORMAT_DEFAULT))
+ .thenReturn(AudioSystem.AUDIO_STATUS_ERROR);
+ runWithBluetoothPrivilegedPermission(
+ () -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo,
+ /*codec*/ AudioSystem.AUDIO_FORMAT_DEFAULT, AudioManager.STREAM_MUSIC));
+
+ assertEquals(0, mDevInventory.getConnectedDevices().size());
+ }
+
+ // test that the device is added when AudioSystem returns AUDIO_STATUS_OK
+ // when setDeviceConnectionState is called for the connection
+ when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE,
+ AudioSystem.AUDIO_FORMAT_DEFAULT))
+ .thenReturn(AudioSystem.AUDIO_STATUS_OK);
+ runWithBluetoothPrivilegedPermission(
+ () -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo,
+ /*codec*/ AudioSystem.AUDIO_FORMAT_DEFAULT, AudioManager.STREAM_MUSIC));
+ assertEquals(1, mDevInventory.getConnectedDevices().size());
+ }
+
+ // TODO add test for hearing aid
+
+ // TODO add test for BLE
+
+ /**
+ * Executes a Runnable while holding the BLUETOOTH_PRIVILEGED permission
+ * @param toRunWithPermission the runnable to run with BT privileges
+ */
+ private void runWithBluetoothPrivilegedPermission(Runnable toRunWithPermission) {
+ try {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(Manifest.permission.BLUETOOTH_PRIVILEGED);
+ toRunWithPermission.run();
+ } finally {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index e5c42082..fb82b872c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -888,7 +888,7 @@
return true;
});
- mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
+ mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
service.addApprovedList("a", 0, true);
service.reregisterService(cn, 0);
@@ -919,7 +919,7 @@
return true;
});
- mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
+ mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
service.addApprovedList("a", 0, false);
service.reregisterService(cn, 0);
@@ -950,7 +950,7 @@
return true;
});
- mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
+ mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
service.addApprovedList("a/a", 0, true);
service.reregisterService(cn, 0);
@@ -981,7 +981,7 @@
return true;
});
- mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
+ mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
service.addApprovedList("a/a", 0, false);
service.reregisterService(cn, 0);
@@ -1211,6 +1211,64 @@
}
@Test
+ public void testUpgradeAppNoIntentFilterNoRebind() throws Exception {
+ Context context = spy(getContext());
+ doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any());
+
+ ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles,
+ mIpm, APPROVAL_BY_COMPONENT);
+
+ List<String> packages = new ArrayList<>();
+ packages.add("package");
+ addExpectedServices(service, packages, 0);
+
+ final ComponentName unapprovedComponent = ComponentName.unflattenFromString("package/C1");
+ final ComponentName approvedComponent = ComponentName.unflattenFromString("package/C2");
+
+ // Both components are approved initially
+ mExpectedPrimaryComponentNames.clear();
+ mExpectedPrimaryPackages.clear();
+ mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2");
+ mExpectedSecondaryComponentNames.clear();
+ mExpectedSecondaryPackages.clear();
+
+ loadXml(service);
+
+ //Component package/C1 loses serviceInterface intent filter
+ ManagedServices.Config config = service.getConfig();
+ when(mPm.queryIntentServicesAsUser(any(), anyInt(), anyInt())).
+ thenAnswer(new Answer<List<ResolveInfo>>() {
+ @Override
+ public List<ResolveInfo> answer(InvocationOnMock invocationOnMock)
+ throws Throwable {
+ Object[] args = invocationOnMock.getArguments();
+ Intent invocationIntent = (Intent) args[0];
+ if (invocationIntent != null) {
+ if (invocationIntent.getAction().equals(config.serviceInterface)
+ && packages.contains(invocationIntent.getPackage())) {
+ List<ResolveInfo> dummyServices = new ArrayList<>();
+ ResolveInfo resolveInfo = new ResolveInfo();
+ ServiceInfo serviceInfo = new ServiceInfo();
+ serviceInfo.packageName = invocationIntent.getPackage();
+ serviceInfo.name = approvedComponent.getClassName();
+ serviceInfo.permission = service.getConfig().bindPermission;
+ resolveInfo.serviceInfo = serviceInfo;
+ dummyServices.add(resolveInfo);
+ return dummyServices;
+ }
+ }
+ return new ArrayList<>();
+ }
+ });
+
+ // Trigger package update
+ service.onPackagesChanged(false, new String[]{"package"}, new int[]{0});
+
+ assertFalse(service.isComponentEnabledForCurrentProfiles(unapprovedComponent));
+ assertTrue(service.isComponentEnabledForCurrentProfiles(approvedComponent));
+ }
+
+ @Test
public void testSetPackageOrComponentEnabled() throws Exception {
for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) {
ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
@@ -1915,7 +1973,7 @@
metaDataAutobindAllow.putBoolean(META_DATA_DEFAULT_AUTOBIND, true);
metaDatas.put(cn_allowed, metaDataAutobindAllow);
- mockServiceInfoWithMetaData(componentNames, service, metaDatas);
+ mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas);
service.addApprovedList(cn_allowed.flattenToString(), 0, true);
service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
@@ -1960,7 +2018,7 @@
metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false);
metaDatas.put(cn_disallowed, metaDataAutobindDisallow);
- mockServiceInfoWithMetaData(componentNames, service, metaDatas);
+ mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas);
service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
@@ -1999,7 +2057,7 @@
metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false);
metaDatas.put(cn_disallowed, metaDataAutobindDisallow);
- mockServiceInfoWithMetaData(componentNames, service, metaDatas);
+ mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas);
service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
@@ -2070,8 +2128,8 @@
}
private void mockServiceInfoWithMetaData(List<ComponentName> componentNames,
- ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas)
- throws RemoteException {
+ ManagedServices service, PackageManager packageManager,
+ ArrayMap<ComponentName, Bundle> metaDatas) throws RemoteException {
when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer(
(Answer<ServiceInfo>) invocation -> {
ComponentName invocationCn = invocation.getArgument(0);
@@ -2086,6 +2144,39 @@
return null;
}
);
+
+ // add components to queryIntentServicesAsUser response
+ final List<String> packages = new ArrayList<>();
+ for (ComponentName cn: componentNames) {
+ packages.add(cn.getPackageName());
+ }
+ ManagedServices.Config config = service.getConfig();
+ when(packageManager.queryIntentServicesAsUser(any(), anyInt(), anyInt())).
+ thenAnswer(new Answer<List<ResolveInfo>>() {
+ @Override
+ public List<ResolveInfo> answer(InvocationOnMock invocationOnMock)
+ throws Throwable {
+ Object[] args = invocationOnMock.getArguments();
+ Intent invocationIntent = (Intent) args[0];
+ if (invocationIntent != null) {
+ if (invocationIntent.getAction().equals(config.serviceInterface)
+ && packages.contains(invocationIntent.getPackage())) {
+ List<ResolveInfo> dummyServices = new ArrayList<>();
+ for (ComponentName cn: componentNames) {
+ ResolveInfo resolveInfo = new ResolveInfo();
+ ServiceInfo serviceInfo = new ServiceInfo();
+ serviceInfo.packageName = invocationIntent.getPackage();
+ serviceInfo.name = cn.getClassName();
+ serviceInfo.permission = service.getConfig().bindPermission;
+ resolveInfo.serviceInfo = serviceInfo;
+ dummyServices.add(resolveInfo);
+ }
+ return dummyServices;
+ }
+ }
+ return new ArrayList<>();
+ }
+ });
}
private void resetComponentsAndPackages() {
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
index 901c036..4f75931 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
@@ -20,7 +20,7 @@
import static android.os.VibrationAttributes.CATEGORY_UNKNOWN;
import static android.os.VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY;
import static android.os.VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF;
-import static android.os.VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE;
+import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK;
import static android.os.VibrationAttributes.USAGE_TOUCH;
import static android.os.VibrationEffect.Composition.PRIMITIVE_CLICK;
import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK;
@@ -346,7 +346,7 @@
}
@Test
- public void testVibrationAttribute_keyboardCategoryOff_isIme_notUseKeyboardCategory() {
+ public void testVibrationAttribute_keyboardCategoryOff_isIme_useTouchUsage() {
mSetFlagsRule.disableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
@@ -362,7 +362,7 @@
}
@Test
- public void testVibrationAttribute_keyboardCategoryOn_notIme_notUseKeyboardCategory() {
+ public void testVibrationAttribute_keyboardCategoryOn_notIme_useTouchUsage() {
mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
@@ -377,7 +377,7 @@
}
@Test
- public void testVibrationAttribute_keyboardCategoryOn_isIme_useKeyboardCategory() {
+ public void testVibrationAttribute_keyboardCategoryOn_isIme_useImeFeedbackUsage() {
mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
@@ -385,64 +385,14 @@
VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
effectId, /* flags */ 0,
HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS);
- assertWithMessage("Expected USAGE_TOUCH for effect " + effectId)
- .that(attrs.getUsage()).isEqualTo(USAGE_TOUCH);
+ assertWithMessage("Expected USAGE_IME_FEEDBACK for effect " + effectId)
+ .that(attrs.getUsage()).isEqualTo(USAGE_IME_FEEDBACK);
assertWithMessage("Expected CATEGORY_KEYBOARD for effect " + effectId)
.that(attrs.getCategory()).isEqualTo(CATEGORY_KEYBOARD);
}
}
@Test
- public void testVibrationAttribute_noFixAmplitude_notBypassIntensityScale() {
- mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
- mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK);
- mockKeyboardVibrationFixedAmplitude(-1);
- HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
-
- for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
- VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
- effectId, /* flags */ 0,
- HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS);
- assertWithMessage("Expected no FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect "
- + effectId)
- .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isFalse();
- }
- }
-
- @Test
- public void testVibrationAttribute_notIme_notBypassIntensityScale() {
- mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
- mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK);
- mockKeyboardVibrationFixedAmplitude(KEYBOARD_VIBRATION_FIXED_AMPLITUDE);
- HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
-
- for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
- VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
- effectId, /* flags */ 0, /* privFlags */ 0);
- assertWithMessage("Expected no FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect "
- + effectId)
- .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isFalse();
- }
- }
-
- @Test
- public void testVibrationAttribute_fixAmplitude_isIme_bypassIntensityScale() {
- mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
- mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK);
- mockKeyboardVibrationFixedAmplitude(KEYBOARD_VIBRATION_FIXED_AMPLITUDE);
- HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
-
- for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
- VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
- effectId, /* flags */ 0,
- HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS);
- assertWithMessage("Expected FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect "
- + effectId)
- .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isTrue();
- }
- }
-
- @Test
public void testIsRestricted_biometricConstants_returnsTrue() {
HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
index 60d8964..8d4a6aa 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
@@ -23,6 +23,7 @@
import static android.os.VibrationAttributes.USAGE_ALARM;
import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK;
+import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK;
import static android.os.VibrationAttributes.USAGE_MEDIA;
import static android.os.VibrationAttributes.USAGE_NOTIFICATION;
import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION;
@@ -893,6 +894,22 @@
}
@Test
+ public void getCurrentIntensity_ImeFeedbackValueReflectsToKeyboardVibrationSettings() {
+ setDefaultIntensity(USAGE_IME_FEEDBACK, VIBRATION_INTENSITY_MEDIUM);
+ setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_HIGH);
+
+ setKeyboardVibrationSettingsSupported(false);
+ mVibrationSettings.update();
+ assertEquals(VIBRATION_INTENSITY_HIGH,
+ mVibrationSettings.getCurrentIntensity(USAGE_IME_FEEDBACK));
+
+ setKeyboardVibrationSettingsSupported(true);
+ mVibrationSettings.update();
+ assertEquals(VIBRATION_INTENSITY_MEDIUM,
+ mVibrationSettings.getCurrentIntensity(USAGE_IME_FEEDBACK));
+ }
+
+ @Test
public void getFallbackEffect_returnsEffectsFromSettings() {
assertNotNull(mVibrationSettings.getFallbackEffect(VibrationEffect.EFFECT_TICK));
assertNotNull(mVibrationSettings.getFallbackEffect(VibrationEffect.EFFECT_TEXTURE_TICK));
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index bea6917..e411a17 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -154,6 +154,9 @@
private static final VibrationAttributes RINGTONE_ATTRS =
new VibrationAttributes.Builder().setUsage(
VibrationAttributes.USAGE_RINGTONE).build();
+ private static final VibrationAttributes IME_FEEDBACK_ATTRS =
+ new VibrationAttributes.Builder().setUsage(
+ VibrationAttributes.USAGE_IME_FEEDBACK).build();
private static final VibrationAttributes UNKNOWN_ATTRS =
new VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_UNKNOWN).build();
@@ -853,6 +856,7 @@
vibrate(service, VibrationEffect.createOneShot(2000, 200),
new VibrationAttributes.Builder().setUsage(
VibrationAttributes.USAGE_UNKNOWN).build());
+ vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), IME_FEEDBACK_ATTRS);
InOrder inOrderVerifier = inOrder(mAppOpsManagerMock);
inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE),
@@ -868,6 +872,8 @@
anyInt(), anyString());
inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE),
eq(AudioAttributes.USAGE_UNKNOWN), anyInt(), anyString());
+ inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE),
+ eq(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION), anyInt(), anyString());
}
@Test
@@ -1684,40 +1690,6 @@
}
@Test
- public void vibrate_withBypassScaleFlag_ignoresIntensitySettingsAndResolvesAmplitude()
- throws Exception {
- // Permission needed for bypassing user settings
- grantPermission(android.Manifest.permission.MODIFY_PHONE_STATE);
-
- int defaultTouchIntensity =
- mVibrator.getDefaultVibrationIntensity(VibrationAttributes.USAGE_TOUCH);
- // This will scale down touch vibrations.
- setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY,
- defaultTouchIntensity > Vibrator.VIBRATION_INTENSITY_LOW
- ? defaultTouchIntensity - 1
- : defaultTouchIntensity);
-
- int defaultAmplitude = mContextSpy.getResources().getInteger(
- com.android.internal.R.integer.config_defaultVibrationAmplitude);
-
- mockVibrators(1);
- FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
- fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
- VibratorManagerService service = createSystemReadyService();
-
- vibrateAndWaitUntilFinished(service,
- VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE),
- new VibrationAttributes.Builder()
- .setUsage(VibrationAttributes.USAGE_TOUCH)
- .setFlags(VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)
- .build());
-
- assertEquals(1, fakeVibrator.getAllEffectSegments().size());
-
- assertEquals(defaultAmplitude / 255f, fakeVibrator.getAmplitudes().get(0), 1e-5);
- }
-
- @Test
@RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
public void vibrate_withAdaptiveHaptics_appliesCorrectAdaptiveScales() throws Exception {
// Keep user settings the same as device default so only adaptive scale is applied.
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index ff1c6c8..d0080d2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -102,6 +102,7 @@
import android.service.voice.IVoiceInteractionSession;
import android.util.Pair;
import android.util.Size;
+import android.view.Display;
import android.view.Gravity;
import android.view.RemoteAnimationAdapter;
import android.window.TaskFragmentOrganizerToken;
@@ -941,6 +942,91 @@
notNull() /* options */);
}
+
+ /**
+ * This test ensures that activity launch on a secondary display is allowed if the activity did
+ * not opt out from showing on remote devices.
+ */
+ @Test
+ public void testStartActivityOnVirtualDisplay() {
+ final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK,
+ false /* mockGetRootTask */);
+ starter.mRequest.activityInfo.flags |= ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+
+ // Create a virtual display at bottom.
+ final TestDisplayContent secondaryDisplay =
+ new TestDisplayContent.Builder(mAtm, 1000, 1500)
+ .setType(Display.TYPE_VIRTUAL)
+ .setPosition(POSITION_BOTTOM).build();
+ final TaskDisplayArea secondaryTaskContainer = secondaryDisplay.getDefaultTaskDisplayArea();
+ final Task stack = secondaryTaskContainer.createRootTask(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+ // Create an activity record on the top of secondary display.
+ final ActivityRecord topActivityOnSecondaryDisplay = createSingleTaskActivityOn(stack);
+
+ // Put an activity on default display as the top focused activity.
+ new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+ // Start activity with the same intent as {@code topActivityOnSecondaryDisplay}
+ // on secondary display.
+ final ActivityOptions options = ActivityOptions.makeBasic()
+ .setLaunchDisplayId(secondaryDisplay.mDisplayId);
+ final int result = starter.setReason("testStartActivityOnVirtualDisplay")
+ .setIntent(topActivityOnSecondaryDisplay.intent)
+ .setActivityOptions(options.toBundle())
+ .execute();
+
+ // Ensure result is delivering intent to top.
+ assertEquals(START_DELIVERED_TO_TOP, result);
+
+ // Ensure secondary display only creates one stack.
+ verify(secondaryTaskContainer, times(1)).createRootTask(anyInt(), anyInt(), anyBoolean());
+ }
+
+ /**
+ * This test ensures that activity launch on a secondary display is disallowed if the activity
+ * opted out from showing on remote devices.
+ */
+ @EnableFlags(android.companion.virtualdevice.flags.Flags
+ .FLAG_ENFORCE_REMOTE_DEVICE_OPT_OUT_ON_ALL_VIRTUAL_DISPLAYS)
+ @Test
+ public void testStartOptedOutActivityOnVirtualDisplay() {
+ final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK,
+ false /* mockGetRootTask */);
+ starter.mRequest.activityInfo.flags &= ~ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+
+ // Create a virtual display at bottom.
+ final TestDisplayContent secondaryDisplay =
+ new TestDisplayContent.Builder(mAtm, 1000, 1500)
+ .setType(Display.TYPE_VIRTUAL)
+ .setPosition(POSITION_BOTTOM).build();
+ final TaskDisplayArea secondaryTaskContainer = secondaryDisplay.getDefaultTaskDisplayArea();
+ final Task stack = secondaryTaskContainer.createRootTask(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+ // Create an activity record on the top of secondary display.
+ final ActivityRecord topActivityOnSecondaryDisplay = createSingleTaskActivityOn(stack);
+
+ // Put an activity on default display as the top focused activity.
+ new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+ // Start activity with the same intent as {@code topActivityOnSecondaryDisplay}
+ // on secondary display.
+ final ActivityOptions options = ActivityOptions.makeBasic()
+ .setLaunchDisplayId(secondaryDisplay.mDisplayId);
+ final int result = starter.setReason("testStartOptedOutActivityOnVirtualDisplay")
+ .setIntent(topActivityOnSecondaryDisplay.intent)
+ .setActivityOptions(options.toBundle())
+ .execute();
+
+ // Ensure result is canceled.
+ assertEquals(START_CANCELED, result);
+
+ // Ensure secondary display only creates one stack.
+ verify(secondaryTaskContainer, times(1)).createRootTask(anyInt(), anyInt(), anyBoolean());
+ }
+
@Test
public void testWasVisibleInRestartAttempt() {
final ActivityStarter starter = prepareStarter(
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
index 57118f2..f843386 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
@@ -63,11 +63,6 @@
private final Message mScreenUnblocker = mock(Message.class);
- @Override
- protected void onBeforeSystemServicesCreated() {
- mSetFlagsRule.enableFlags(Flags.FLAG_DEFER_DISPLAY_UPDATES);
- }
-
@Before
public void before() {
doReturn(true).when(mDisplayContent).getLastHasContent();
diff --git a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java b/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java
deleted file mode 100644
index 78509db..0000000
--- a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import static com.android.internal.R.bool.config_unfoldTransitionEnabled;
-import static com.android.server.wm.DeviceStateController.DeviceState.REAR;
-import static com.android.server.wm.DeviceStateController.DeviceState.FOLDED;
-import static com.android.server.wm.DeviceStateController.DeviceState.HALF_FOLDED;
-import static com.android.server.wm.DeviceStateController.DeviceState.OPEN;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.when;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Tests for the {@link WindowToken} class.
- *
- * Build/Install/Run:
- * atest WmTests:PhysicalDisplaySwitchTransitionLauncherTest
- */
-@SmallTest
-@Presubmit
-@RunWith(WindowTestRunner.class)
-public class PhysicalDisplaySwitchTransitionLauncherTest extends WindowTestsBase {
-
- @Mock
- Context mContext;
- @Mock
- Resources mResources;
- @Mock
- BLASTSyncEngine mSyncEngine;
-
- WindowTestsBase.TestTransitionPlayer mPlayer;
- TransitionController mTransitionController;
- DisplayContent mDisplayContent;
-
- private PhysicalDisplaySwitchTransitionLauncher mTarget;
- private float mOriginalAnimationScale;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- mTransitionController = new WindowTestsBase.TestTransitionController(mAtm);
- mTransitionController.setSyncEngine(mSyncEngine);
- mPlayer = new WindowTestsBase.TestTransitionPlayer(
- mTransitionController, mAtm.mWindowOrganizerController);
- when(mContext.getResources()).thenReturn(mResources);
- mDisplayContent = new TestDisplayContent.Builder(mAtm, 100, 150).build();
- mTarget = new PhysicalDisplaySwitchTransitionLauncher(mDisplayContent, mAtm, mContext,
- mTransitionController);
- mOriginalAnimationScale = ValueAnimator.getDurationScale();
- }
-
- @After
- public void after() {
- ValueAnimator.setDurationScale(mOriginalAnimationScale);
- }
-
- @Test
- public void testDisplaySwitchAfterUnfoldToOpen_animationsEnabled_requestsTransition() {
- givenAllAnimationsEnabled();
- mTarget.foldStateChanged(FOLDED);
-
- mTarget.foldStateChanged(OPEN);
- final Rect origBounds = new Rect();
- mDisplayContent.getBounds(origBounds);
- origBounds.offsetTo(0, 0);
- mTarget.requestDisplaySwitchTransitionIfNeeded(
- mDisplayContent.getDisplayId(),
- origBounds.width(),
- origBounds.height(),
- /* newDisplayWidth= */ 200,
- /* newDisplayHeight= */ 250
- );
-
- assertNotNull(mPlayer.mLastRequest);
- assertEquals(mDisplayContent.getDisplayId(),
- mPlayer.mLastRequest.getDisplayChange().getDisplayId());
- assertEquals(origBounds, mPlayer.mLastRequest.getDisplayChange().getStartAbsBounds());
- assertEquals(new Rect(0, 0, 200, 250),
- mPlayer.mLastRequest.getDisplayChange().getEndAbsBounds());
- }
-
- @Test
- public void testDisplaySwitchAfterFolding_animationEnabled_doesNotRequestTransition() {
- givenAllAnimationsEnabled();
- mTarget.foldStateChanged(OPEN);
-
- mTarget.foldStateChanged(FOLDED);
- requestDisplaySwitch();
-
- assertTransitionNotRequested();
- }
-
- @Test
- public void testDisplaySwitchAfterUnfoldingToHalf_animationEnabled_requestsTransition() {
- givenAllAnimationsEnabled();
- mTarget.foldStateChanged(FOLDED);
-
- mTarget.foldStateChanged(HALF_FOLDED);
- requestDisplaySwitch();
-
- assertTransitionRequested();
- }
-
- @Test
- public void testDisplaySwitchSecondTimeAfterUnfolding_animationEnabled_noTransition() {
- givenAllAnimationsEnabled();
- mTarget.foldStateChanged(FOLDED);
- mTarget.foldStateChanged(OPEN);
- requestDisplaySwitch();
- mPlayer.mLastRequest = null;
-
- requestDisplaySwitch();
-
- assertTransitionNotRequested();
- }
-
-
- @Test
- public void testDisplaySwitchAfterGoingToRearAndBack_animationEnabled_noTransition() {
- givenAllAnimationsEnabled();
- mTarget.foldStateChanged(OPEN);
-
- mTarget.foldStateChanged(REAR);
- mTarget.foldStateChanged(OPEN);
- requestDisplaySwitch();
-
- assertTransitionNotRequested();
- }
-
- @Test
- public void testDisplaySwitchAfterUnfoldingAndFolding_animationEnabled_noTransition() {
- givenAllAnimationsEnabled();
- mTarget.foldStateChanged(FOLDED);
- mTarget.foldStateChanged(OPEN);
- // No request display switch event (simulate very fast fold after unfold, even before
- // the displays switched)
- mTarget.foldStateChanged(FOLDED);
-
- requestDisplaySwitch();
-
- assertTransitionNotRequested();
- }
-
- @Test
- public void testDisplaySwitch_whenShellTransitionsNotEnabled_noTransition() {
- givenAllAnimationsEnabled();
- givenShellTransitionsEnabled(false);
- mTarget.foldStateChanged(FOLDED);
-
- mTarget.foldStateChanged(OPEN);
- requestDisplaySwitch();
-
- assertTransitionNotRequested();
- }
-
- @Test
- public void testDisplaySwitch_whenAnimationsDisabled_noTransition() {
- givenAllAnimationsEnabled();
- givenAnimationsEnabled(false);
- mTarget.foldStateChanged(FOLDED);
-
- mTarget.foldStateChanged(OPEN);
- requestDisplaySwitch();
-
- assertTransitionNotRequested();
- }
-
- @Test
- public void testDisplaySwitch_whenUnfoldAnimationDisabled_noTransition() {
- givenAllAnimationsEnabled();
- givenUnfoldTransitionEnabled(false);
- mTarget.foldStateChanged(FOLDED);
-
- mTarget.foldStateChanged(OPEN);
- requestDisplaySwitch();
-
- assertTransitionNotRequested();
- }
-
- @Test
- public void testDisplaySwitchAfterUnfolding_otherCollectingTransition_collectsDisplaySwitch() {
- givenAllAnimationsEnabled();
- mTarget.foldStateChanged(FOLDED);
-
- mTarget.foldStateChanged(OPEN);
- requestDisplaySwitch();
-
- // Collects to the current transition
- assertTrue(mTransitionController.getCollectingTransition().mParticipants.contains(
- mDisplayContent));
- }
-
-
- @Test
- public void testDisplaySwitch_whenNoContentInDisplayContent_noTransition() {
- givenAllAnimationsEnabled();
- givenDisplayContentHasContent(false);
- mTarget.foldStateChanged(FOLDED);
-
- mTarget.foldStateChanged(OPEN);
- requestDisplaySwitch();
-
- assertTransitionNotRequested();
- }
-
- private void assertTransitionRequested() {
- assertNotNull(mPlayer.mLastRequest);
- }
-
- private void assertTransitionNotRequested() {
- assertNull(mPlayer.mLastRequest);
- }
-
- private void requestDisplaySwitch() {
- mTarget.requestDisplaySwitchTransitionIfNeeded(
- mDisplayContent.getDisplayId(),
- mDisplayContent.getBounds().width(),
- mDisplayContent.getBounds().height(),
- /* newDisplayWidth= */ 200,
- /* newDisplayHeight= */ 250
- );
- }
-
- private void givenAllAnimationsEnabled() {
- givenAnimationsEnabled(true);
- givenUnfoldTransitionEnabled(true);
- givenShellTransitionsEnabled(true);
- givenDisplayContentHasContent(true);
- }
-
- private void givenUnfoldTransitionEnabled(boolean enabled) {
- when(mResources.getBoolean(config_unfoldTransitionEnabled)).thenReturn(enabled);
- }
-
- private void givenAnimationsEnabled(boolean enabled) {
- ValueAnimator.setDurationScale(enabled ? 1.0f : 0.0f);
- }
-
- private void givenShellTransitionsEnabled(boolean enabled) {
- if (enabled) {
- mTransitionController.registerTransitionPlayer(mPlayer, null /* proc */);
- } else {
- mTransitionController.unregisterTransitionPlayer(mPlayer);
- }
- }
-
- private void givenDisplayContentHasContent(boolean hasContent) {
- when(mDisplayContent.getLastHasContent()).thenReturn(hasContent);
- }
-}