summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/app/NotificationManager.java3
-rw-r--r--core/java/android/app/supervision/flags.aconfig8
-rw-r--r--core/java/android/appwidget/AppWidgetManager.java18
-rw-r--r--core/java/android/hardware/input/InputSettings.java37
-rw-r--r--core/java/android/os/Parcel.java16
-rw-r--r--core/java/android/provider/Settings.java30
-rw-r--r--core/java/android/util/proto/ProtoFieldFilter.java335
-rw-r--r--core/java/android/widget/EditText.java4
-rw-r--r--core/java/android/window/ITaskOrganizerController.aidl7
-rw-r--r--core/java/android/window/WindowContainerTransaction.java37
-rw-r--r--core/proto/android/providers/settings/system.proto3
-rw-r--r--core/tests/coretests/src/android/app/NotificationManagerTest.java53
-rw-r--r--core/tests/utiltests/src/android/util/proto/ProtoFieldFilterTest.java230
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt162
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java53
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java15
-rw-r--r--media/java/android/media/MediaCodecInfo.java8
-rw-r--r--media/java/android/media/flags/media_better_together.aconfig12
-rw-r--r--media/java/android/media/tv/TvInputService.java21
-rw-r--r--media/java/android/media/tv/TvInputServiceExtensionManager.java88
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java1
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java1
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java1
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java1
-rw-r--r--packages/SystemUI/Android.bp6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt74
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt1110
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt125
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt101
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt122
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt51
-rw-r--r--packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt300
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt292
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt4
-rw-r--r--packages/SystemUI/utils/Android.bp1
-rw-r--r--packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/LatestConflated.kt3
-rw-r--r--ravenwood/texts/ravenwood-annotation-allowed-classes.txt1
-rw-r--r--services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java2
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java13
-rw-r--r--services/core/java/com/android/server/BootReceiver.java129
-rw-r--r--services/core/java/com/android/server/input/InputSettingsObserver.java7
-rw-r--r--services/core/java/com/android/server/input/NativeInputManagerService.java5
-rw-r--r--services/core/java/com/android/server/os/NativeTombstoneManager.java22
-rw-r--r--services/core/java/com/android/server/os/core_os_flags.aconfig2
-rw-r--r--services/core/java/com/android/server/wm/WindowOrganizerController.java18
-rw-r--r--services/core/jni/com_android_server_input_InputManagerService.cpp29
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java28
-rw-r--r--telecomm/java/android/telecom/Call.java7
58 files changed, 1480 insertions, 2230 deletions
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 05b3316a5133..1e1ec602d0a2 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1656,7 +1656,8 @@ public class NotificationManager {
if (Flags.modesApi() && Flags.modesUi()) {
PackageManager pm = mContext.getPackageManager();
return !pm.hasSystemFeature(PackageManager.FEATURE_WATCH)
- && !pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ && !pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
+ && !pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
} else {
return false;
}
diff --git a/core/java/android/app/supervision/flags.aconfig b/core/java/android/app/supervision/flags.aconfig
index 4ee3a0360b43..18182b804627 100644
--- a/core/java/android/app/supervision/flags.aconfig
+++ b/core/java/android/app/supervision/flags.aconfig
@@ -40,3 +40,11 @@ flag {
description: "Flag to enable the SupervisionAppService"
bug: "389123070"
}
+
+flag {
+ name: "enable_supervision_settings_screen"
+ is_exported: true
+ namespace: "supervision"
+ description: "Flag that enables the Supervision settings screen with top-level Android settings entry point"
+ bug: "383404606"
+}
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index 67ad4594599f..b54e17beb100 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -1684,10 +1684,14 @@ public class AppWidgetManager {
private IBinder mIBinder;
ConnectionTask(@NonNull FilterComparison filter) {
- mContext.bindService(filter.getIntent(),
- Context.BindServiceFlags.of(Context.BIND_AUTO_CREATE),
- mHandler::post,
- this);
+ try {
+ mContext.bindService(filter.getIntent(),
+ Context.BindServiceFlags.of(Context.BIND_AUTO_CREATE),
+ mHandler::post,
+ this);
+ } catch (Exception e) {
+ Log.e(TAG, "Error connecting to service in connection cache", e);
+ }
}
@Override
@@ -1737,7 +1741,11 @@ public class AppWidgetManager {
handleNext();
return;
}
- mContext.unbindService(this);
+ try {
+ mContext.unbindService(this);
+ } catch (Exception e) {
+ Log.e(TAG, "Error unbinding the cached connection", e);
+ }
mActiveConnections.values().remove(this);
}
}
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index b380e259577c..cd48047bccb4 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -385,6 +385,42 @@ public class InputSettings {
}
/**
+ * Whether touchpad acceleration is enabled or not.
+ *
+ * @param context The application context.
+ *
+ * @hide
+ */
+ public static boolean isTouchpadAccelerationEnabled(@NonNull Context context) {
+ if (!isPointerAccelerationFeatureFlagEnabled()) {
+ return false;
+ }
+
+ return Settings.System.getIntForUser(context.getContentResolver(),
+ Settings.System.TOUCHPAD_ACCELERATION_ENABLED, 1, UserHandle.USER_CURRENT)
+ == 1;
+ }
+
+ /**
+ * Enables or disables touchpad acceleration.
+ *
+ * @param context The application context.
+ * @param enabled Will enable touchpad acceleration if true, disable it if
+ * false.
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+ public static void setTouchpadAccelerationEnabled(@NonNull Context context,
+ boolean enabled) {
+ if (!isPointerAccelerationFeatureFlagEnabled()) {
+ return;
+ }
+ Settings.System.putIntForUser(context.getContentResolver(),
+ Settings.System.TOUCHPAD_ACCELERATION_ENABLED, enabled ? 1 : 0,
+ UserHandle.USER_CURRENT);
+ }
+
+ /**
* Returns true if the feature flag for disabling system gestures on touchpads is enabled.
*
* @hide
@@ -835,7 +871,6 @@ public class InputSettings {
UserHandle.USER_CURRENT);
}
-
/**
* Whether Accessibility bounce keys feature is enabled.
*
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 4aa74621bd62..c6a63a7ef238 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -50,6 +50,7 @@ import com.android.internal.util.ArrayUtils;
import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
+import dalvik.annotation.optimization.NeverInline;
import libcore.util.SneakyThrow;
@@ -628,6 +629,19 @@ public final class Parcel {
}
}
+ @NeverInline
+ private void errorUsedWhileRecycling() {
+ String error = "Parcel used while recycled. "
+ + Log.getStackTraceString(new Throwable())
+ + " Original recycle call (if DEBUG_RECYCLE): ", mStack;
+ Log.wtf(TAG, error);
+ // TODO(b/381155347): harder error
+ }
+
+ private void assertNotRecycled() {
+ if (mRecycled) errorUsedWhileRecycling();
+ }
+
/**
* Set a {@link ReadWriteHelper}, which can be used to avoid having duplicate strings, for
* example.
@@ -1180,6 +1194,7 @@ public final class Parcel {
* growing dataCapacity() if needed.
*/
public final void writeInt(int val) {
+ assertNotRecycled();
int err = nativeWriteInt(mNativePtr, val);
if (err != OK) {
nativeSignalExceptionForError(err);
@@ -3282,6 +3297,7 @@ public final class Parcel {
* Read an integer value from the parcel at the current dataPosition().
*/
public final int readInt() {
+ assertNotRecycled();
return nativeReadInt(mNativePtr);
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index a22333bd1812..2b18b01734d3 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -353,6 +353,18 @@ public final class Settings {
*/
public static final String ACTION_ONE_HANDED_SETTINGS =
"android.settings.action.ONE_HANDED_SETTINGS";
+
+ /**
+ * Activity Action: Show Double tap power gesture Settings page.
+ * <p>
+ * Input: Nothing
+ * <p>
+ * Output: Nothing
+ * @hide
+ */
+ public static final String ACTION_DOUBLE_TAP_POWER_SETTINGS =
+ "android.settings.action.DOUBLE_TAP_POWER_SETTINGS";
+
/**
* The return values for {@link Settings.Config#set}
* @hide
@@ -6318,6 +6330,17 @@ public final class Settings {
public static final String TOUCHPAD_SYSTEM_GESTURES = "touchpad_system_gestures";
/**
+ * Whether touchpad acceleration is enabled.
+ *
+ * When enabled, the speed of the pointer will increase as the user moves their
+ * finger faster on the touchpad.
+ *
+ * @hide
+ */
+ public static final String TOUCHPAD_ACCELERATION_ENABLED =
+ "touchpad_acceleration_enabled";
+
+ /**
* Whether to enable reversed vertical scrolling for connected mice.
*
* When enabled, scrolling down on the mouse wheel will move the screen up and vice versa.
@@ -6612,6 +6635,7 @@ public final class Settings {
PRIVATE_SETTINGS.add(TOUCHPAD_TAP_DRAGGING);
PRIVATE_SETTINGS.add(TOUCHPAD_RIGHT_CLICK_ZONE);
PRIVATE_SETTINGS.add(TOUCHPAD_SYSTEM_GESTURES);
+ PRIVATE_SETTINGS.add(TOUCHPAD_ACCELERATION_ENABLED);
PRIVATE_SETTINGS.add(CAMERA_FLASH_NOTIFICATION);
PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION);
PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION_COLOR);
@@ -12356,12 +12380,6 @@ public final class Settings {
public static final String CAMERA_EXTENSIONS_FALLBACK = "camera_extensions_fallback";
/**
- * Controls whether contextual suggestions can be shown in the media controls.
- * @hide
- */
- public static final String MEDIA_CONTROLS_RECOMMENDATION = "qs_media_recommend";
-
- /**
* Controls magnification mode when magnification is enabled via a system-wide triple tap
* gesture or the accessibility shortcut.
*
diff --git a/core/java/android/util/proto/ProtoFieldFilter.java b/core/java/android/util/proto/ProtoFieldFilter.java
new file mode 100644
index 000000000000..c3ae106b68f8
--- /dev/null
+++ b/core/java/android/util/proto/ProtoFieldFilter.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.proto;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.function.Predicate;
+
+/**
+ * A utility class that reads raw protobuf data from an InputStream
+ * and copies only those fields for which a given predicate returns true.
+ *
+ * <p>
+ * This is a low-level approach that does not fully decode fields
+ * (unless necessary to determine lengths). It simply:
+ * <ul>
+ * <li>Parses each field's tag (varint for field number & wire type)</li>
+ * <li>If {@code includeFn(fieldNumber) == true}, copies
+ * the tag bytes and the field bytes directly to the output</li>
+ * <li>Otherwise, skips that field in the input</li>
+ * </ul>
+ * </p>
+ *
+ * <p>
+ * Because we do not re-encode, unknown or unrecognized fields are copied
+ * <i>verbatim</i> and remain exactly as in the input (useful for partial
+ * parsing or partial transformations).
+ * </p>
+ *
+ * <p>
+ * Note: This class only filters based on top-level field numbers. For length-delimited
+ * fields (including nested messages), the entire contents are either copied or skipped
+ * as a single unit. The class is not capable of nested filtering.
+ * </p>
+ *
+ * @hide
+ */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
+public class ProtoFieldFilter {
+
+ private static final int BUFFER_SIZE_BYTES = 4096;
+
+ private final Predicate<Integer> mFieldPredicate;
+ // General-purpose buffer for reading proto fields and their data
+ private final byte[] mBuffer;
+ // Buffer specifically designated to hold varint values (max 10 bytes in protobuf encoding)
+ private final byte[] mVarIntBuffer = new byte[10];
+
+ /**
+ * Constructs a ProtoFieldFilter with a predicate that considers depth.
+ *
+ * @param fieldPredicate A predicate returning true if the given fieldNumber should be
+ * included in the output.
+ * @param bufferSize The size of the internal buffer used for processing proto fields.
+ * Larger buffers may improve performance when processing large
+ * length-delimited fields.
+ */
+ public ProtoFieldFilter(Predicate<Integer> fieldPredicate, int bufferSize) {
+ this.mFieldPredicate = fieldPredicate;
+ this.mBuffer = new byte[bufferSize];
+ }
+
+ /**
+ * Constructs a ProtoFieldFilter with a predicate that considers depth and
+ * uses a default buffer size.
+ *
+ * @param fieldPredicate A predicate returning true if the given fieldNumber should be
+ * included in the output.
+ */
+ public ProtoFieldFilter(Predicate<Integer> fieldPredicate) {
+ this(fieldPredicate, BUFFER_SIZE_BYTES);
+ }
+
+ /**
+ * Reads raw protobuf data from {@code in} and writes only those fields
+ * passing {@code includeFn} to {@code out}. The predicate is given
+ * (fieldNumber, wireType) for each encountered field.
+ *
+ * @param in The input stream of protobuf data
+ * @param out The output stream to which we write the filtered protobuf
+ * @throws IOException If reading or writing fails, or if the protobuf data is corrupted
+ */
+ public void filter(InputStream in, OutputStream out) throws IOException {
+ int tagBytesLength;
+ while ((tagBytesLength = readRawVarint(in)) > 0) {
+ // Parse the varint loaded in mVarIntBuffer, through readRawVarint
+ long tagVal = parseVarint(mVarIntBuffer, tagBytesLength);
+ int fieldNumber = (int) (tagVal >>> ProtoStream.FIELD_ID_SHIFT);
+ int wireType = (int) (tagVal & ProtoStream.WIRE_TYPE_MASK);
+
+ if (fieldNumber == 0) {
+ break;
+ }
+ if (mFieldPredicate.test(fieldNumber)) {
+ out.write(mVarIntBuffer, 0, tagBytesLength);
+ copyFieldData(in, out, wireType);
+ } else {
+ skipFieldData(in, wireType);
+ }
+ }
+ }
+
+ /**
+ * Reads a varint (up to 10 bytes) from the stream as raw bytes
+ * and returns it in a byte array. If the stream is at EOF, returns null.
+ *
+ * @param in The input stream
+ * @return the size of the varint bytes moved to mVarIntBuffer
+ * @throws IOException If an error occurs, or if we detect a malformed varint
+ */
+ private int readRawVarint(InputStream in) throws IOException {
+ // We attempt to read 1 byte. If none available => null
+ int b = in.read();
+ if (b < 0) {
+ return 0;
+ }
+ int count = 0;
+ mVarIntBuffer[count++] = (byte) b;
+ // If the continuation bit is set, we continue
+ while ((b & 0x80) != 0) {
+ // read next byte
+ b = in.read();
+ // EOF
+ if (b < 0) {
+ throw new IOException("Malformed varint: reached EOF mid-varint");
+ }
+ // max 10 bytes for varint 64
+ if (count >= 10) {
+ throw new IOException("Malformed varint: too many bytes (max 10)");
+ }
+ mVarIntBuffer[count++] = (byte) b;
+ }
+ return count;
+ }
+
+ /**
+ * Parses a varint from the given raw bytes and returns it as a long.
+ *
+ * @param rawVarint The bytes representing the varint
+ * @param byteLength The number of bytes to read from rawVarint
+ * @return The decoded long value
+ */
+ private static long parseVarint(byte[] rawVarint, int byteLength) throws IOException {
+ long result = 0;
+ int shift = 0;
+ for (int i = 0; i < byteLength; i++) {
+ result |= ((rawVarint[i] & 0x7F) << shift);
+ shift += 7;
+ if (shift > 63) {
+ throw new IOException("Malformed varint: exceeds 64 bits");
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Copies the wire data for a single field from {@code in} to {@code out},
+ * assuming we have already read the field's tag.
+ *
+ * @param in The input stream (protobuf data)
+ * @param out The output stream
+ * @param wireType The wire type (0=varint, 1=fixed64, 2=length-delim, 5=fixed32)
+ * @throws IOException if reading/writing fails or data is malformed
+ */
+ private void copyFieldData(InputStream in, OutputStream out, int wireType)
+ throws IOException {
+ switch (wireType) {
+ case ProtoStream.WIRE_TYPE_VARINT:
+ copyVarint(in, out);
+ break;
+ case ProtoStream.WIRE_TYPE_FIXED64:
+ copyFixed(in, out, 8);
+ break;
+ case ProtoStream.WIRE_TYPE_LENGTH_DELIMITED:
+ copyLengthDelimited(in, out);
+ break;
+ case ProtoStream.WIRE_TYPE_FIXED32:
+ copyFixed(in, out, 4);
+ break;
+ // case WIRE_TYPE_START_GROUP:
+ // Not Supported
+ // case WIRE_TYPE_END_GROUP:
+ // Not Supported
+ default:
+ // Error or unrecognized wire type
+ throw new IOException("Unknown or unsupported wire type: " + wireType);
+ }
+ }
+
+ /**
+ * Skips the wire data for a single field from {@code in},
+ * assuming the field's tag was already read.
+ */
+ private void skipFieldData(InputStream in, int wireType) throws IOException {
+ switch (wireType) {
+ case ProtoStream.WIRE_TYPE_VARINT:
+ skipVarint(in);
+ break;
+ case ProtoStream.WIRE_TYPE_FIXED64:
+ skipBytes(in, 8);
+ break;
+ case ProtoStream.WIRE_TYPE_LENGTH_DELIMITED:
+ skipLengthDelimited(in);
+ break;
+ case ProtoStream.WIRE_TYPE_FIXED32:
+ skipBytes(in, 4);
+ break;
+ // case WIRE_TYPE_START_GROUP:
+ // Not Supported
+ // case WIRE_TYPE_END_GROUP:
+ // Not Supported
+ default:
+ throw new IOException("Unknown or unsupported wire type: " + wireType);
+ }
+ }
+
+ /** Copies a varint (the field's value) from in to out. */
+ private static void copyVarint(InputStream in, OutputStream out) throws IOException {
+ while (true) {
+ int b = in.read();
+ if (b < 0) {
+ throw new IOException("EOF while copying varint");
+ }
+ out.write(b);
+ if ((b & 0x80) == 0) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Copies exactly {@code length} bytes from {@code in} to {@code out}.
+ */
+ private void copyFixed(InputStream in, OutputStream out,
+ int length) throws IOException {
+ int toRead = length;
+ while (toRead > 0) {
+ int chunk = Math.min(toRead, mBuffer.length);
+ int readCount = in.read(mBuffer, 0, chunk);
+ if (readCount < 0) {
+ throw new IOException("EOF while copying fixed" + (length * 8) + " field");
+ }
+ out.write(mBuffer, 0, readCount);
+ toRead -= readCount;
+ }
+ }
+
+ /** Copies a length-delimited field */
+ private void copyLengthDelimited(InputStream in,
+ OutputStream out) throws IOException {
+ // 1) read length varint (and copy)
+ int lengthVarintLength = readRawVarint(in);
+ if (lengthVarintLength <= 0) {
+ throw new IOException("EOF reading length for length-delimited field");
+ }
+ out.write(mVarIntBuffer, 0, lengthVarintLength);
+
+ long lengthVal = parseVarint(mVarIntBuffer, lengthVarintLength);
+ if (lengthVal < 0 || lengthVal > Integer.MAX_VALUE) {
+ throw new IOException("Invalid length for length-delimited field: " + lengthVal);
+ }
+
+ // 2) copy that many bytes
+ copyFixed(in, out, (int) lengthVal);
+ }
+
+ /** Skips a varint in the input (does not write anything). */
+ private static void skipVarint(InputStream in) throws IOException {
+ int bytesSkipped = 0;
+ while (true) {
+ int b = in.read();
+ if (b < 0) {
+ throw new IOException("EOF while skipping varint");
+ }
+ if ((b & 0x80) == 0) {
+ break;
+ }
+ bytesSkipped++;
+ if (bytesSkipped > 10) {
+ throw new IOException("Malformed varint: exceeds maximum length of 10 bytes");
+ }
+ }
+ }
+
+ /** Skips exactly n bytes. */
+ private void skipBytes(InputStream in, long n) throws IOException {
+ long skipped = in.skip(n);
+ // If skip fails, fallback to reading the remaining bytes
+ if (skipped < n) {
+ long bytesRemaining = n - skipped;
+
+ while (bytesRemaining > 0) {
+ int bytesToRead = (int) Math.min(bytesRemaining, mBuffer.length);
+ int bytesRead = in.read(mBuffer, 0, bytesToRead);
+ if (bytesRemaining < 0) {
+ throw new IOException("EOF while skipping bytes");
+ }
+ bytesRemaining -= bytesRead;
+ }
+ }
+ }
+
+ /**
+ * Skips a length-delimited field.
+ * 1) read the length as varint,
+ * 2) skip that many bytes
+ */
+ private void skipLengthDelimited(InputStream in) throws IOException {
+ int lengthVarintLength = readRawVarint(in);
+ if (lengthVarintLength <= 0) {
+ throw new IOException("EOF reading length for length-delimited field");
+ }
+ long lengthVal = parseVarint(mVarIntBuffer, lengthVarintLength);
+ if (lengthVal < 0 || lengthVal > Integer.MAX_VALUE) {
+ throw new IOException("Invalid length to skip: " + lengthVal);
+ }
+ skipBytes(in, lengthVal);
+ }
+
+}
diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java
index 3e0161a9b791..2056e2254ecd 100644
--- a/core/java/android/widget/EditText.java
+++ b/core/java/android/widget/EditText.java
@@ -57,8 +57,8 @@ import com.android.internal.R;
* Choosing the input type configures the keyboard type that is shown, acceptable characters,
* and appearance of the edit text.
* For example, if you want to accept a secret number, like a unique pin or serial number,
- * you can set inputType to "numericPassword".
- * An inputType of "numericPassword" results in an edit text that accepts numbers only,
+ * you can set inputType to {@link android.R.styleable#TextView_inputType numberPassword}.
+ * An input type of {@code numberPassword} results in an edit text that accepts numbers only,
* shows a numeric keyboard when focused, and masks the text that is entered for privacy.
* <p>
* See the <a href="{@docRoot}guide/topics/ui/controls/text.html">Text Fields</a>
diff --git a/core/java/android/window/ITaskOrganizerController.aidl b/core/java/android/window/ITaskOrganizerController.aidl
index 1748b9d38538..8934cf67b380 100644
--- a/core/java/android/window/ITaskOrganizerController.aidl
+++ b/core/java/android/window/ITaskOrganizerController.aidl
@@ -39,7 +39,12 @@ interface ITaskOrganizerController {
*/
void unregisterTaskOrganizer(ITaskOrganizer organizer);
- /** Creates a persistent root task in WM for a particular windowing-mode. */
+ /**
+ * Creates a persistent root task in WM for a particular windowing-mode.
+ *
+ * It may be removed using {@link #deleteRootTask} or through
+ * {@link WindowContainerTransaction#removeRootTask}.
+ */
void createRootTask(int displayId, int windowingMode, IBinder launchCookie,
boolean removeWithTaskOrganizer);
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 02f8e2f59b33..ce0ccd5c6d0d 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -614,6 +614,10 @@ public final class WindowContainerTransaction implements Parcelable {
/**
* Finds and removes a task and its children using its container token. The task is removed
* from recents.
+ *
+ * If the task is a root task, its leaves are removed but the root task is not. Use
+ * {@link #removeRootTask(WindowContainerToken)} to remove the root task.
+ *
* @param containerToken ContainerToken of Task to be removed
*/
@NonNull
@@ -623,6 +627,19 @@ public final class WindowContainerTransaction implements Parcelable {
}
/**
+ * Finds and removes a root task created by an organizer and its leaves using its container
+ * token.
+ *
+ * @param containerToken ContainerToken of the root task to be removed
+ * @hide
+ */
+ @NonNull
+ public WindowContainerTransaction removeRootTask(@NonNull WindowContainerToken containerToken) {
+ mHierarchyOps.add(HierarchyOp.createForRemoveRootTask(containerToken.asBinder()));
+ return this;
+ }
+
+ /**
* Sets whether a container is being drag-resized.
* When {@code true}, the client will reuse a single (larger) surface size to avoid
* continuous allocations on every size change.
@@ -1573,6 +1590,7 @@ public final class WindowContainerTransaction implements Parcelable {
public static final int HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES = 21;
public static final int HIERARCHY_OP_TYPE_SET_KEYGUARD_STATE = 22;
public static final int HIERARCHY_OP_TYPE_SET_DISABLE_LAUNCH_ADJACENT = 23;
+ public static final int HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK = 24;
@IntDef(prefix = {"HIERARCHY_OP_TYPE_"}, value = {
HIERARCHY_OP_TYPE_REPARENT,
@@ -1598,7 +1616,8 @@ public final class WindowContainerTransaction implements Parcelable {
HIERARCHY_OP_TYPE_RESTORE_BACK_NAVIGATION,
HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES,
HIERARCHY_OP_TYPE_SET_KEYGUARD_STATE,
- HIERARCHY_OP_TYPE_SET_DISABLE_LAUNCH_ADJACENT
+ HIERARCHY_OP_TYPE_SET_DISABLE_LAUNCH_ADJACENT,
+ HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK,
})
@Retention(RetentionPolicy.SOURCE)
public @interface HierarchyOpType {
@@ -1795,6 +1814,18 @@ public final class WindowContainerTransaction implements Parcelable {
.build();
}
+ /**
+ * Creates a hierarchy op for deleting a root task
+ *
+ * @hide
+ **/
+ @NonNull
+ public static HierarchyOp createForRemoveRootTask(@NonNull IBinder container) {
+ return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK)
+ .setContainer(container)
+ .build();
+ }
+
/** Creates a hierarchy op for clearing adjacent root tasks. */
@NonNull
public static HierarchyOp createForClearAdjacentRoots(@NonNull IBinder root) {
@@ -2012,6 +2043,7 @@ public final class WindowContainerTransaction implements Parcelable {
return "removeInsetsFrameProvider";
case HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP: return "setAlwaysOnTop";
case HIERARCHY_OP_TYPE_REMOVE_TASK: return "removeTask";
+ case HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK: return "removeRootTask";
case HIERARCHY_OP_TYPE_FINISH_ACTIVITY: return "finishActivity";
case HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS: return "clearAdjacentRoots";
case HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH:
@@ -2096,6 +2128,9 @@ public final class WindowContainerTransaction implements Parcelable {
case HIERARCHY_OP_TYPE_REMOVE_TASK:
sb.append("task=").append(mContainer);
break;
+ case HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK:
+ sb.append("rootTask=").append(mContainer);
+ break;
case HIERARCHY_OP_TYPE_FINISH_ACTIVITY:
sb.append("activity=").append(mContainer);
break;
diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto
index 64c9f540a97b..325790c22fce 100644
--- a/core/proto/android/providers/settings/system.proto
+++ b/core/proto/android/providers/settings/system.proto
@@ -218,7 +218,8 @@ message SystemSettingsProto {
optional SettingProto tap_to_click = 4 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto tap_dragging = 5 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto three_finger_tap_customization = 6 [ (android.privacy).dest = DEST_AUTOMATIC ];
- optional SettingProto system_gestures = 7 [ (android.privacy).dest = DEST_AUTOMATIC ];;
+ optional SettingProto system_gestures = 7 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ optional SettingProto acceleration_enabled = 8 [ (android.privacy).dest = DEST_AUTOMATIC ];;
}
optional Touchpad touchpad = 36;
diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java
index a201f1fe6d89..41432294b3c2 100644
--- a/core/tests/coretests/src/android/app/NotificationManagerTest.java
+++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java
@@ -30,8 +30,10 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.annotation.Nullable;
import android.content.Context;
import android.content.ContextWrapper;
+import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.os.UserHandle;
import android.platform.test.annotations.EnableFlags;
@@ -441,6 +443,46 @@ public class NotificationManagerTest {
.getOrCreateNotificationChannels(any(), any(), anyInt(), anyBoolean());
}
+ @Test
+ @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ public void areAutomaticZenRulesUserManaged_handheld_isTrue() {
+ PackageManager pm = mock(PackageManager.class);
+ when(pm.hasSystemFeature(any())).thenReturn(false);
+ mContext.setPackageManager(pm);
+
+ assertThat(mNotificationManager.areAutomaticZenRulesUserManaged()).isTrue();
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ public void areAutomaticZenRulesUserManaged_auto_isFalse() {
+ PackageManager pm = mock(PackageManager.class);
+ when(pm.hasSystemFeature(eq(PackageManager.FEATURE_AUTOMOTIVE))).thenReturn(true);
+ mContext.setPackageManager(pm);
+
+ assertThat(mNotificationManager.areAutomaticZenRulesUserManaged()).isFalse();
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ public void areAutomaticZenRulesUserManaged_tv_isFalse() {
+ PackageManager pm = mock(PackageManager.class);
+ when(pm.hasSystemFeature(eq(PackageManager.FEATURE_LEANBACK))).thenReturn(true);
+ mContext.setPackageManager(pm);
+
+ assertThat(mNotificationManager.areAutomaticZenRulesUserManaged()).isFalse();
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ public void areAutomaticZenRulesUserManaged_watch_isFalse() {
+ PackageManager pm = mock(PackageManager.class);
+ when(pm.hasSystemFeature(eq(PackageManager.FEATURE_WATCH))).thenReturn(true);
+ mContext.setPackageManager(pm);
+
+ assertThat(mNotificationManager.areAutomaticZenRulesUserManaged()).isFalse();
+ }
+
private Notification exampleNotification() {
return new Notification.Builder(mContext, "channel")
.setSmallIcon(android.R.drawable.star_big_on)
@@ -470,6 +512,7 @@ public class NotificationManagerTest {
// Helper context wrapper class where we can control just the return values of getPackageName,
// getOpPackageName, and getUserId (used in getNotificationChannels).
private static class PackageTestableContext extends ContextWrapper {
+ private PackageManager mPm;
private String mPackage;
private String mOpPackage;
private Integer mUserId;
@@ -478,6 +521,10 @@ public class NotificationManagerTest {
super(base);
}
+ void setPackageManager(@Nullable PackageManager pm) {
+ mPm = pm;
+ }
+
void setParameters(String packageName, String opPackageName, int userId) {
mPackage = packageName;
mOpPackage = opPackageName;
@@ -485,6 +532,12 @@ public class NotificationManagerTest {
}
@Override
+ public PackageManager getPackageManager() {
+ if (mPm != null) return mPm;
+ return super.getPackageManager();
+ }
+
+ @Override
public String getPackageName() {
if (mPackage != null) return mPackage;
return super.getPackageName();
diff --git a/core/tests/utiltests/src/android/util/proto/ProtoFieldFilterTest.java b/core/tests/utiltests/src/android/util/proto/ProtoFieldFilterTest.java
new file mode 100644
index 000000000000..76d0aaaa4309
--- /dev/null
+++ b/core/tests/utiltests/src/android/util/proto/ProtoFieldFilterTest.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.proto;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+
+/**
+ * Unit tests for {@link android.util.proto.ProtoFieldFilter}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksCoreTests:ProtoFieldFilterTest
+ *
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ProtoFieldFilterTest {
+
+ private static final class FieldTypes {
+ static final long INT64 = ProtoStream.FIELD_TYPE_INT64 | ProtoStream.FIELD_COUNT_SINGLE;
+ static final long FIXED64 = ProtoStream.FIELD_TYPE_FIXED64 | ProtoStream.FIELD_COUNT_SINGLE;
+ static final long BYTES = ProtoStream.FIELD_TYPE_BYTES | ProtoStream.FIELD_COUNT_SINGLE;
+ static final long FIXED32 = ProtoStream.FIELD_TYPE_FIXED32 | ProtoStream.FIELD_COUNT_SINGLE;
+ static final long MESSAGE = ProtoStream.FIELD_TYPE_MESSAGE | ProtoStream.FIELD_COUNT_SINGLE;
+ static final long INT32 = ProtoStream.FIELD_TYPE_INT32 | ProtoStream.FIELD_COUNT_SINGLE;
+ }
+
+ private static ProtoOutputStream createBasicTestProto() {
+ ProtoOutputStream out = new ProtoOutputStream();
+
+ out.writeInt64(ProtoStream.makeFieldId(1, FieldTypes.INT64), 12345L);
+ out.writeFixed64(ProtoStream.makeFieldId(2, FieldTypes.FIXED64), 0x1234567890ABCDEFL);
+ out.writeBytes(ProtoStream.makeFieldId(3, FieldTypes.BYTES), new byte[]{1, 2, 3, 4, 5});
+ out.writeFixed32(ProtoStream.makeFieldId(4, FieldTypes.FIXED32), 0xDEADBEEF);
+
+ return out;
+ }
+
+ private static byte[] filterProto(byte[] input, ProtoFieldFilter filter) throws IOException {
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(input);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ filter.filter(inputStream, outputStream);
+ return outputStream.toByteArray();
+ }
+
+ @Test
+ public void testNoFieldsFiltered() throws IOException {
+ byte[] input = createBasicTestProto().getBytes();
+ byte[] output = filterProto(input, new ProtoFieldFilter(fieldNumber -> true));
+ assertArrayEquals("No fields should be filtered out", input, output);
+ }
+
+ @Test
+ public void testAllFieldsFiltered() throws IOException {
+ byte[] input = createBasicTestProto().getBytes();
+ byte[] output = filterProto(input, new ProtoFieldFilter(fieldNumber -> false));
+
+ assertEquals("All fields should be filtered out", 0, output.length);
+ }
+
+ @Test
+ public void testSpecificFieldsFiltered() throws IOException {
+
+ ProtoOutputStream out = createBasicTestProto();
+ byte[] output = filterProto(out.getBytes(), new ProtoFieldFilter(n -> n != 2));
+
+ ProtoInputStream in = new ProtoInputStream(output);
+ boolean[] fieldsFound = new boolean[5];
+
+ int fieldNumber;
+ while ((fieldNumber = in.nextField()) != ProtoInputStream.NO_MORE_FIELDS) {
+ fieldsFound[fieldNumber] = true;
+ switch (fieldNumber) {
+ case 1:
+ assertEquals(12345L, in.readLong(ProtoStream.makeFieldId(1, FieldTypes.INT64)));
+ break;
+ case 2:
+ fail("Field 2 should be filtered out");
+ break;
+ case 3:
+ assertArrayEquals(new byte[]{1, 2, 3, 4, 5},
+ in.readBytes(ProtoStream.makeFieldId(3, FieldTypes.BYTES)));
+ break;
+ case 4:
+ assertEquals(0xDEADBEEF,
+ in.readInt(ProtoStream.makeFieldId(4, FieldTypes.FIXED32)));
+ break;
+ default:
+ fail("Unexpected field number: " + fieldNumber);
+ }
+ }
+
+ assertTrue("Field 1 should be present", fieldsFound[1]);
+ assertFalse("Field 2 should be filtered", fieldsFound[2]);
+ assertTrue("Field 3 should be present", fieldsFound[3]);
+ assertTrue("Field 4 should be present", fieldsFound[4]);
+ }
+
+ @Test
+ public void testDifferentWireTypes() throws IOException {
+ ProtoOutputStream out = new ProtoOutputStream();
+
+ out.writeInt64(ProtoStream.makeFieldId(1, FieldTypes.INT64), 12345L);
+ out.writeFixed64(ProtoStream.makeFieldId(2, FieldTypes.FIXED64), 0x1234567890ABCDEFL);
+ out.writeBytes(ProtoStream.makeFieldId(3, FieldTypes.BYTES), new byte[]{10, 20, 30});
+
+ long token = out.start(ProtoStream.makeFieldId(4, FieldTypes.MESSAGE));
+ out.writeInt32(ProtoStream.makeFieldId(1, FieldTypes.INT32), 42);
+ out.end(token);
+
+ out.writeFixed32(ProtoStream.makeFieldId(5, FieldTypes.FIXED32), 0xDEADBEEF);
+
+ byte[] output = filterProto(out.getBytes(), new ProtoFieldFilter(fieldNumber -> true));
+
+ ProtoInputStream in = new ProtoInputStream(output);
+ boolean[] fieldsFound = new boolean[6];
+
+ int fieldNumber;
+ while ((fieldNumber = in.nextField()) != ProtoInputStream.NO_MORE_FIELDS) {
+ fieldsFound[fieldNumber] = true;
+ switch (fieldNumber) {
+ case 1:
+ assertEquals(12345L, in.readLong(ProtoStream.makeFieldId(1, FieldTypes.INT64)));
+ break;
+ case 2:
+ assertEquals(0x1234567890ABCDEFL,
+ in.readLong(ProtoStream.makeFieldId(2, FieldTypes.FIXED64)));
+ break;
+ case 3:
+ assertArrayEquals(new byte[]{10, 20, 30},
+ in.readBytes(ProtoStream.makeFieldId(3, FieldTypes.BYTES)));
+ break;
+ case 4:
+ token = in.start(ProtoStream.makeFieldId(4, FieldTypes.MESSAGE));
+ assertTrue(in.nextField() == 1);
+ assertEquals(42, in.readInt(ProtoStream.makeFieldId(1, FieldTypes.INT32)));
+ assertTrue(in.nextField() == ProtoInputStream.NO_MORE_FIELDS);
+ in.end(token);
+ break;
+ case 5:
+ assertEquals(0xDEADBEEF,
+ in.readInt(ProtoStream.makeFieldId(5, FieldTypes.FIXED32)));
+ break;
+ default:
+ fail("Unexpected field number: " + fieldNumber);
+ }
+ }
+
+ assertTrue("All fields should be present",
+ fieldsFound[1] && fieldsFound[2] && fieldsFound[3]
+ && fieldsFound[4] && fieldsFound[5]);
+ }
+ @Test
+ public void testNestedMessagesUnfiltered() throws IOException {
+ ProtoOutputStream out = new ProtoOutputStream();
+
+ out.writeInt64(ProtoStream.makeFieldId(1, FieldTypes.INT64), 12345L);
+
+ long token = out.start(ProtoStream.makeFieldId(2, FieldTypes.MESSAGE));
+ out.writeInt32(ProtoStream.makeFieldId(1, FieldTypes.INT32), 6789);
+ out.writeFixed32(ProtoStream.makeFieldId(2, FieldTypes.FIXED32), 0xCAFEBABE);
+ out.end(token);
+
+ byte[] output = filterProto(out.getBytes(), new ProtoFieldFilter(n -> n != 2));
+
+ // Verify output
+ ProtoInputStream in = new ProtoInputStream(output);
+ boolean[] fieldsFound = new boolean[3];
+
+ int fieldNumber;
+ while ((fieldNumber = in.nextField()) != ProtoInputStream.NO_MORE_FIELDS) {
+ fieldsFound[fieldNumber] = true;
+ if (fieldNumber == 1) {
+ assertEquals(12345L, in.readLong(ProtoStream.makeFieldId(1, FieldTypes.INT64)));
+ } else {
+ fail("Unexpected field number: " + fieldNumber);
+ }
+ }
+
+ assertTrue("Field 1 should be present", fieldsFound[1]);
+ assertFalse("Field 2 should be filtered out", fieldsFound[2]);
+ }
+
+ @Test
+ public void testRepeatedFields() throws IOException {
+
+ ProtoOutputStream out = new ProtoOutputStream();
+ long fieldId = ProtoStream.makeFieldId(1,
+ ProtoStream.FIELD_TYPE_INT32 | ProtoStream.FIELD_COUNT_REPEATED);
+
+ out.writeRepeatedInt32(fieldId, 100);
+ out.writeRepeatedInt32(fieldId, 200);
+ out.writeRepeatedInt32(fieldId, 300);
+
+ byte[] input = out.getBytes();
+
+ byte[] output = filterProto(input, new ProtoFieldFilter(fieldNumber -> true));
+
+ assertArrayEquals("Repeated fields should be preserved", input, output);
+ }
+
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
index a7eebd6159e4..9d445f0bb80d 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
@@ -36,6 +36,8 @@ import com.android.internal.protolog.ProtoLog
import com.android.launcher3.icons.BubbleIconFactory
import com.android.wm.shell.Flags
import com.android.wm.shell.R
+import com.android.wm.shell.bubbles.BubbleStackView.SurfaceSynchronizer
+import com.android.wm.shell.bubbles.Bubbles.BubbleExpandListener
import com.android.wm.shell.bubbles.Bubbles.SysuiProxy
import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix
import com.android.wm.shell.common.FloatingContentCoordinator
@@ -75,6 +77,7 @@ class BubbleStackViewTest {
private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory
private lateinit var bubbleData: BubbleData
private lateinit var bubbleStackViewManager: FakeBubbleStackViewManager
+ private lateinit var surfaceSynchronizer: FakeSurfaceSynchronizer
private var sysuiProxy = mock<SysuiProxy>()
@Before
@@ -108,13 +111,14 @@ class BubbleStackViewTest {
bubbleStackViewManager = FakeBubbleStackViewManager()
expandedViewManager = FakeBubbleExpandedViewManager()
bubbleTaskViewFactory = FakeBubbleTaskViewFactory(context, shellExecutor)
+ surfaceSynchronizer = FakeSurfaceSynchronizer()
bubbleStackView =
BubbleStackView(
context,
bubbleStackViewManager,
positioner,
bubbleData,
- null,
+ surfaceSynchronizer,
FloatingContentCoordinator(),
{ sysuiProxy },
shellExecutor
@@ -309,6 +313,7 @@ class BubbleStackViewTest {
@Test
fun tapDifferentBubble_shouldReorder() {
+ surfaceSynchronizer.isActive = false
val bubble1 = createAndInflateChatBubble(key = "bubble1")
val bubble2 = createAndInflateChatBubble(key = "bubble2")
InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -378,6 +383,147 @@ class BubbleStackViewTest {
.inOrder()
}
+ @Test
+ fun tapDifferentBubble_imeVisible_shouldWaitForIme() {
+ val bubble1 = createAndInflateChatBubble(key = "bubble1")
+ val bubble2 = createAndInflateChatBubble(key = "bubble2")
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleStackView.addBubble(bubble1)
+ bubbleStackView.addBubble(bubble2)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+ assertThat(bubbleStackView.bubbleCount).isEqualTo(2)
+ assertThat(bubbleData.bubbles).hasSize(2)
+ assertThat(bubbleData.selectedBubble).isEqualTo(bubble2)
+ assertThat(bubble2.iconView).isNotNull()
+
+ val expandListener = FakeBubbleExpandListener()
+ bubbleStackView.setExpandListener(expandListener)
+
+ var lastUpdate: BubbleData.Update? = null
+ val semaphore = Semaphore(0)
+ val listener =
+ BubbleData.Listener { update ->
+ lastUpdate = update
+ semaphore.release()
+ }
+ bubbleData.setListener(listener)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubble2.iconView!!.performClick()
+ assertThat(bubbleData.isExpanded).isTrue()
+
+ bubbleStackView.setSelectedBubble(bubble2)
+ bubbleStackView.isExpanded = true
+ shellExecutor.flushAll()
+ }
+
+ assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+ assertThat(lastUpdate!!.expanded).isTrue()
+ assertThat(lastUpdate!!.bubbles.map { it.key })
+ .containsExactly("bubble2", "bubble1")
+ .inOrder()
+
+ // wait for idle to allow the animation to start
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // wait for the expansion animation to complete before interacting with the bubbles
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
+ AnimatableScaleMatrix.SCALE_X, AnimatableScaleMatrix.SCALE_Y)
+
+ // make the IME visible and tap on bubble1 to select it
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ positioner.setImeVisible(true, 100)
+ bubble1.iconView!!.performClick()
+ // we have to set the selected bubble in the stack view manually because we don't have a
+ // listener wired up.
+ bubbleStackView.setSelectedBubble(bubble1)
+ shellExecutor.flushAll()
+ }
+
+ val onImeHidden = bubbleStackViewManager.onImeHidden
+ assertThat(onImeHidden).isNotNull()
+
+ assertThat(expandListener.bubblesExpandedState).isEqualTo(mapOf("bubble2" to true))
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ onImeHidden!!.run()
+ shellExecutor.flushAll()
+ }
+
+ assertThat(expandListener.bubblesExpandedState)
+ .isEqualTo(mapOf("bubble1" to true, "bubble2" to false))
+ assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+ assertThat(bubbleData.selectedBubble).isEqualTo(bubble1)
+ }
+
+ @Test
+ fun tapDifferentBubble_imeHidden_updatesImmediately() {
+ val bubble1 = createAndInflateChatBubble(key = "bubble1")
+ val bubble2 = createAndInflateChatBubble(key = "bubble2")
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleStackView.addBubble(bubble1)
+ bubbleStackView.addBubble(bubble2)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+ assertThat(bubbleStackView.bubbleCount).isEqualTo(2)
+ assertThat(bubbleData.bubbles).hasSize(2)
+ assertThat(bubbleData.selectedBubble).isEqualTo(bubble2)
+ assertThat(bubble2.iconView).isNotNull()
+
+ val expandListener = FakeBubbleExpandListener()
+ bubbleStackView.setExpandListener(expandListener)
+
+ var lastUpdate: BubbleData.Update? = null
+ val semaphore = Semaphore(0)
+ val listener =
+ BubbleData.Listener { update ->
+ lastUpdate = update
+ semaphore.release()
+ }
+ bubbleData.setListener(listener)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubble2.iconView!!.performClick()
+ assertThat(bubbleData.isExpanded).isTrue()
+
+ bubbleStackView.setSelectedBubble(bubble2)
+ bubbleStackView.isExpanded = true
+ shellExecutor.flushAll()
+ }
+
+ assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+ assertThat(lastUpdate!!.expanded).isTrue()
+ assertThat(lastUpdate!!.bubbles.map { it.key })
+ .containsExactly("bubble2", "bubble1")
+ .inOrder()
+
+ // wait for idle to allow the animation to start
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // wait for the expansion animation to complete before interacting with the bubbles
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
+ AnimatableScaleMatrix.SCALE_X, AnimatableScaleMatrix.SCALE_Y)
+
+ // make the IME hidden and tap on bubble1 to select it
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ positioner.setImeVisible(false, 0)
+ bubble1.iconView!!.performClick()
+ // we have to set the selected bubble in the stack view manually because we don't have a
+ // listener wired up.
+ bubbleStackView.setSelectedBubble(bubble1)
+ shellExecutor.flushAll()
+ }
+
+ val onImeHidden = bubbleStackViewManager.onImeHidden
+ assertThat(onImeHidden).isNull()
+
+ assertThat(expandListener.bubblesExpandedState)
+ .isEqualTo(mapOf("bubble1" to true, "bubble2" to false))
+ assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+ assertThat(bubbleData.selectedBubble).isEqualTo(bubble1)
+ }
+
@EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
@Test
fun testCreateStackView_noOverflowContents_noOverflow() {
@@ -563,4 +709,18 @@ class BubbleStackViewTest {
this.onImeHidden = onImeHidden
}
}
+
+ private class FakeBubbleExpandListener : BubbleExpandListener {
+ val bubblesExpandedState = mutableMapOf<String, Boolean>()
+ override fun onBubbleExpandChanged(isExpanding: Boolean, key: String) {
+ bubblesExpandedState[key] = isExpanding
+ }
+ }
+
+ private class FakeSurfaceSynchronizer : SurfaceSynchronizer {
+ var isActive = true
+ override fun syncSurfaceAndRun(callback: Runnable) {
+ if (isActive) callback.run()
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 12dfbd9607e4..30d679006c98 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1440,9 +1440,9 @@ public class BubbleController implements ConfigurationChangeListener,
*
* @param intent the intent for the bubble.
*/
- public void expandStackAndSelectBubble(Intent intent) {
+ public void expandStackAndSelectBubble(Intent intent, UserHandle user) {
if (!Flags.enableBubbleAnything()) return;
- Bubble b = mBubbleData.getOrCreateBubble(intent); // Removes from overflow
+ Bubble b = mBubbleData.getOrCreateBubble(intent, user); // Removes from overflow
ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", intent);
if (b.isInflated()) {
mBubbleData.setSelectedBubbleAndExpandStack(b);
@@ -2649,8 +2649,8 @@ public class BubbleController implements ConfigurationChangeListener,
}
@Override
- public void showAppBubble(Intent intent) {
- mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent));
+ public void showAppBubble(Intent intent, UserHandle user) {
+ mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent, user));
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index dc2025bb2dce..76d91ede7aa3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -461,10 +461,8 @@ public class BubbleData {
return bubbleToReturn;
}
- Bubble getOrCreateBubble(Intent intent) {
- UserHandle user = UserHandle.of(mCurrentUserId);
- String bubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(),
- user);
+ Bubble getOrCreateBubble(Intent intent, UserHandle user) {
+ String bubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), user);
Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey);
if (bubbleToReturn == null) {
bubbleToReturn = Bubble.createAppBubble(intent, user, null, mMainExecutor, mBgExecutor);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 979d958e3c5f..1094c290df06 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -2183,34 +2183,39 @@ public class BubbleStackView extends FrameLayout
ProtoLog.d(WM_SHELL_BUBBLES, "showNewlySelectedBubble b=%s, previouslySelected=%s,"
+ " mIsExpanded=%b", newlySelectedKey, previouslySelectedKey, mIsExpanded);
if (mIsExpanded) {
- hideCurrentInputMethod();
-
- if (Flags.enableRetrievableBubbles()) {
- if (mBubbleData.getBubbles().size() == 1) {
- // First bubble, check if overflow visibility needs to change
- updateOverflowVisibility();
+ Runnable onImeHidden = () -> {
+ if (Flags.enableRetrievableBubbles()) {
+ if (mBubbleData.getBubbles().size() == 1) {
+ // First bubble, check if overflow visibility needs to change
+ updateOverflowVisibility();
+ }
}
- }
- // Make the container of the expanded view transparent before removing the expanded view
- // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the
- // expanded view becomes visible on the screen. See b/126856255
- mExpandedViewContainer.setAlpha(0.0f);
- mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
- if (previouslySelected != null) {
- previouslySelected.setTaskViewVisibility(false);
- }
+ // Make the container of the expanded view transparent before removing the expanded
+ // view from it. Otherwise a punch hole created by {@link android.view.SurfaceView}
+ // in the expanded view becomes visible on the screen. See b/126856255
+ mExpandedViewContainer.setAlpha(0.0f);
+ mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
+ if (previouslySelected != null) {
+ previouslySelected.setTaskViewVisibility(false);
+ }
- updateExpandedBubble();
- requestUpdate();
+ updateExpandedBubble();
+ requestUpdate();
- logBubbleEvent(previouslySelected,
- FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
- logBubbleEvent(bubbleToSelect,
- FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
- notifyExpansionChanged(previouslySelected, false /* expanded */);
- notifyExpansionChanged(bubbleToSelect, true /* expanded */);
- });
+ logBubbleEvent(previouslySelected,
+ FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
+ logBubbleEvent(bubbleToSelect,
+ FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
+ notifyExpansionChanged(previouslySelected, false /* expanded */);
+ notifyExpansionChanged(bubbleToSelect, true /* expanded */);
+ });
+ };
+ if (mPositioner.isImeVisible()) {
+ hideCurrentInputMethod(onImeHidden);
+ } else {
+ onImeHidden.run();
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
index 9c2d35431554..0a4d79a6c297 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
@@ -17,8 +17,9 @@
package com.android.wm.shell.bubbles;
import android.content.Intent;
-import android.graphics.Rect;
import android.content.pm.ShortcutInfo;
+import android.graphics.Rect;
+import android.os.UserHandle;
import com.android.wm.shell.bubbles.IBubblesListener;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
@@ -52,7 +53,7 @@ interface IBubbles {
oneway void showShortcutBubble(in ShortcutInfo info) = 12;
- oneway void showAppBubble(in Intent intent) = 13;
+ oneway void showAppBubble(in Intent intent, in UserHandle user) = 13;
oneway void showExpandedView() = 14;
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index ce640b5e5195..ffcc3446d436 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -445,6 +445,15 @@ public class BubbleDataTest extends ShellTestCase {
assertThat(update.updatedBubble.showFlyout()).isFalse();
}
+ @Test
+ public void getOrCreateBubble_withIntent_usesCorrectUser() {
+ Intent intent = new Intent();
+ intent.setPackage(mContext.getPackageName());
+ Bubble b = mBubbleData.getOrCreateBubble(intent, UserHandle.of(/* userId= */ 10));
+
+ assertThat(b.getUser().getIdentifier()).isEqualTo(10);
+ }
+
//
// Overflow
//
@@ -1441,12 +1450,6 @@ public class BubbleDataTest extends ShellTestCase {
assertWithMessage("selectedBubble").that(update.selectedBubble).isEqualTo(bubble);
}
- private void assertSelectionCleared() {
- BubbleData.Update update = mUpdateCaptor.getValue();
- assertWithMessage("selectionChanged").that(update.selectionChanged).isTrue();
- assertWithMessage("selectedBubble").that(update.selectedBubble).isNull();
- }
-
private void assertExpandedChangedTo(boolean expected) {
BubbleData.Update update = mUpdateCaptor.getValue();
assertWithMessage("expandedChanged").that(update.expandedChanged).isTrue();
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java
index 302969f58ba8..9bb31d0076c9 100644
--- a/media/java/android/media/MediaCodecInfo.java
+++ b/media/java/android/media/MediaCodecInfo.java
@@ -908,13 +908,13 @@ public final class MediaCodecInfo {
/** @hide */
public String[] validFeatures() {
Feature[] features = getValidFeatures();
- String[] res = new String[features.length];
- for (int i = 0; i < res.length; i++) {
+ ArrayList<String> res = new ArrayList();
+ for (int i = 0; i < features.length; i++) {
if (!features[i].mInternal) {
- res[i] = features[i].mName;
+ res.add(features[i].mName);
}
}
- return res;
+ return res.toArray(new String[0]);
}
private Feature[] getValidFeatures() {
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index c48b5f4e4aea..312f78e2b24e 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -173,6 +173,18 @@ flag {
bug: "281072508"
}
+
+flag {
+ name: "enable_singleton_audio_manager_route_controller"
+ is_exported: true
+ namespace: "media_solutions"
+ description: "Use singleton AudioManagerRouteController shared across all users."
+ bug: "372868909"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
flag {
name: "enable_use_of_bluetooth_device_get_alias_for_mr2info_get_name"
namespace: "media_solutions"
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index abfc24413d56..f352a4183111 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -161,11 +161,6 @@ public abstract class TvInputService extends Service {
new RemoteCallbackList<>();
private TvInputManager mTvInputManager;
- /**
- * @hide
- */
- protected TvInputServiceExtensionManager mTvInputServiceExtensionManager =
- new TvInputServiceExtensionManager();
@Override
public final IBinder onBind(Intent intent) {
@@ -230,12 +225,20 @@ public abstract class TvInputService extends Service {
@Override
public IBinder getExtensionInterface(String name) {
- if (tifExtensionStandardization() && name != null) {
- if (TvInputServiceExtensionManager.checkIsStandardizedInterfaces(name)) {
- return mTvInputServiceExtensionManager.getExtensionIBinder(name);
+ IBinder binder = TvInputService.this.getExtensionInterface(name);
+ if (tifExtensionStandardization()) {
+ if (name != null
+ && TvInputServiceExtensionManager.checkIsStandardizedInterfaces(name)) {
+ if (TvInputServiceExtensionManager.checkIsStandardizedIBinder(name,
+ binder)) {
+ return binder;
+ } else {
+ // binder with standardized name is not standardized
+ return null;
+ }
}
}
- return TvInputService.this.getExtensionInterface(name);
+ return binder;
}
@Override
diff --git a/media/java/android/media/tv/TvInputServiceExtensionManager.java b/media/java/android/media/tv/TvInputServiceExtensionManager.java
index d33ac9256a21..02d261610d8a 100644
--- a/media/java/android/media/tv/TvInputServiceExtensionManager.java
+++ b/media/java/android/media/tv/TvInputServiceExtensionManager.java
@@ -16,13 +16,9 @@
package android.media.tv;
-import android.annotation.FlaggedApi;
-import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.RequiresPermission;
import android.annotation.StringDef;
-import android.media.tv.flags.Flags;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
@@ -30,21 +26,17 @@ import android.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
/**
* This class provides a list of available standardized TvInputService extension interface names
- * and a container storing IBinder objects that implement these interfaces created by SoC/OEMs.
- * It also provides an API for SoC/OEMs to register implemented IBinder objects.
+ * and checks if IBinder objects created by SoC/OEMs implement these interfaces.
*
* @hide
*/
-@FlaggedApi(Flags.FLAG_TIF_EXTENSION_STANDARDIZATION)
public final class TvInputServiceExtensionManager {
private static final String TAG = "TvInputServiceExtensionManager";
private static final String SCAN_PACKAGE = "android.media.tv.extension.scan.";
@@ -63,33 +55,6 @@ public final class TvInputServiceExtensionManager {
private static final String ANALOG_PACKAGE = "android.media.tv.extension.analog.";
private static final String TUNE_PACKAGE = "android.media.tv.extension.tune.";
- @IntDef(prefix = {"REGISTER_"}, value = {
- REGISTER_SUCCESS,
- REGISTER_FAIL_NAME_NOT_STANDARDIZED,
- REGISTER_FAIL_IMPLEMENTATION_NOT_STANDARDIZED,
- REGISTER_FAIL_REMOTE_EXCEPTION
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface RegisterResult {}
-
- /**
- * Registering binder returns success when it abides standardized interface structure
- */
- public static final int REGISTER_SUCCESS = 0;
- /**
- * Registering binder returns failure when the extension name is not in the standardization
- * list
- */
- public static final int REGISTER_FAIL_NAME_NOT_STANDARDIZED = 1;
- /**
- * Registering binder returns failure when the IBinder does not implement standardized interface
- */
- public static final int REGISTER_FAIL_IMPLEMENTATION_NOT_STANDARDIZED = 2;
- /**
- * Registering binder returns failure when remote server is not available
- */
- public static final int REGISTER_FAIL_REMOTE_EXCEPTION = 3;
-
@StringDef({
ISCAN_INTERFACE,
ISCAN_SESSION,
@@ -673,12 +638,6 @@ public final class TvInputServiceExtensionManager {
IMUX_TUNE
));
- // Store the mapping between interface names and IBinder
- private Map<String, IBinder> mExtensionInterfaceIBinderMapping = new HashMap<>();
-
- TvInputServiceExtensionManager() {
- }
-
/**
* Function to return available extension interface names
*/
@@ -694,43 +653,18 @@ public final class TvInputServiceExtensionManager {
}
/**
- * Registers IBinder objects that implement standardized AIDL interfaces.
- * <p>This function should be used by SoCs/OEMs
- *
- * @param extensionName Extension Interface Name
- * @param binder IBinder object to be registered
- * @return {@link #REGISTER_SUCCESS} on success of registering IBinder object
- * {@link #REGISTER_FAIL_NAME_NOT_STANDARDIZED} on failure due to registering extension
- * with non-standardized name
- * {@link #REGISTER_FAIL_IMPLEMENTATION_NOT_STANDARDIZED} on failure due to IBinder not
- * implementing standardized AIDL interface
- * {@link #REGISTER_FAIL_REMOTE_EXCEPTION} on failure due to remote exception
- */
- @RequiresPermission(android.Manifest.permission.TV_INPUT_HARDWARE)
- @RegisterResult
- public int registerExtensionIBinder(@StandardizedExtensionName @NonNull String extensionName,
- @NonNull IBinder binder) {
- if (!checkIsStandardizedInterfaces(extensionName)) {
- return REGISTER_FAIL_NAME_NOT_STANDARDIZED;
- }
- try {
- if (binder.getInterfaceDescriptor().equals(extensionName)) {
- mExtensionInterfaceIBinderMapping.put(extensionName, binder);
- return REGISTER_SUCCESS;
- } else {
- return REGISTER_FAIL_IMPLEMENTATION_NOT_STANDARDIZED;
+ * Function check if the IBinder object implements standardized interface
+ */
+ public static boolean checkIsStandardizedIBinder(@NonNull String extensionName,
+ @Nullable IBinder binder) {
+ if (binder != null) {
+ try {
+ return binder.getInterfaceDescriptor().equals(extensionName);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Fetching IBinder object failure due to " + e);
}
- } catch (RemoteException e) {
- Log.e(TAG, "Fetching IBinder object failure due to " + e);
- return REGISTER_FAIL_REMOTE_EXCEPTION;
}
- }
-
- /**
- * Function to get corresponding IBinder object
- */
- @Nullable IBinder getExtensionIBinder(@NonNull String extensionName) {
- return mExtensionInterfaceIBinderMapping.get(extensionName);
+ return false;
}
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index d367748d7dbf..640829eacf9f 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -204,7 +204,6 @@ public class SecureSettings {
Settings.Secure.AWARE_TAP_PAUSE_TOUCH_COUNT,
Settings.Secure.PEOPLE_STRIP,
Settings.Secure.MEDIA_CONTROLS_RESUME,
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index 1f56f10cca7d..cf0447f9fb3a 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -117,6 +117,7 @@ public class SystemSettings {
Settings.System.TOUCHPAD_TAP_TO_CLICK,
Settings.System.TOUCHPAD_TAP_DRAGGING,
Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE,
+ Settings.System.TOUCHPAD_ACCELERATION_ENABLED,
Settings.System.CAMERA_FLASH_NOTIFICATION,
Settings.System.SCREEN_FLASH_NOTIFICATION,
Settings.System.SCREEN_FLASH_NOTIFICATION_COLOR,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 242bdce0d79e..9fd0cc8e2812 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -312,7 +312,6 @@ public class SecureSettingsValidators {
VALIDATORS.put(Secure.TAP_GESTURE, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.PEOPLE_STRIP, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.MEDIA_CONTROLS_RESUME, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.MEDIA_CONTROLS_RECOMMENDATION, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.MEDIA_CONTROLS_LOCK_SCREEN, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
new InclusiveIntegerRangeValidator(
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index 4d98a11bdfe7..4f649ed49be3 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -234,6 +234,7 @@ public class SystemSettingsValidators {
VALIDATORS.put(System.TOUCHPAD_TAP_DRAGGING, BOOLEAN_VALIDATOR);
VALIDATORS.put(System.TOUCHPAD_RIGHT_CLICK_ZONE, BOOLEAN_VALIDATOR);
VALIDATORS.put(System.TOUCHPAD_SYSTEM_GESTURES, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(System.TOUCHPAD_ACCELERATION_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(System.LOCK_TO_APP_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(
System.EGG_MODE,
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 848ea0f077ba..0224b297e005 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -534,7 +534,10 @@ android_library {
"-Adagger.useBindingGraphFix=ENABLED",
"-Aroom.schemaLocation=frameworks/base/packages/SystemUI/schemas",
],
- kotlincflags: ["-Xjvm-default=all"],
+ kotlincflags: [
+ "-Xjvm-default=all",
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+ ],
plugins: [
"androidx.room_room-compiler-plugin",
@@ -757,6 +760,7 @@ android_library {
"-Xjvm-default=all",
// TODO(b/352363800): Why do we need this?
"-J-Xmx8192M",
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
],
javacflags: [
"-Adagger.useBindingGraphFix=ENABLED",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
index e531e654cd34..00d5afe26f0a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
@@ -16,14 +16,17 @@
package com.android.systemui.communal
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import android.service.dream.dreamManager
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled
import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -48,12 +51,14 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@EnableFlags(Flags.FLAG_COMMUNAL_HUB)
-@RunWith(AndroidJUnit4::class)
-class CommunalDreamStartableTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class CommunalDreamStartableTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
@@ -63,26 +68,50 @@ class CommunalDreamStartableTest : SysuiTestCase() {
private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
private val powerRepository by lazy { kosmos.fakePowerRepository }
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
@Before
fun setUp() {
kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
underTest =
CommunalDreamStartable(
- powerInteractor = kosmos.powerInteractor,
- communalSettingsInteractor = kosmos.communalSettingsInteractor,
- keyguardInteractor = kosmos.keyguardInteractor,
- keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
- dreamManager = dreamManager,
- communalSceneInteractor = kosmos.communalSceneInteractor,
- bgScope = kosmos.applicationCoroutineScope,
- )
- .apply { start() }
+ powerInteractor = kosmos.powerInteractor,
+ communalSettingsInteractor = kosmos.communalSettingsInteractor,
+ keyguardInteractor = kosmos.keyguardInteractor,
+ keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
+ dreamManager = dreamManager,
+ communalSceneInteractor = kosmos.communalSceneInteractor,
+ bgScope = kosmos.applicationCoroutineScope,
+ )
}
+ @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
+ @Test
+ fun dreamNotStartedWhenTransitioningToHub() =
+ testScope.runTest {
+ // Enable v2 flag and recreate + rerun start method.
+ kosmos.setCommunalV2Enabled(true)
+ underTest.start()
+
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardRepository.setDreaming(false)
+ powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
+ whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true)
+ runCurrent()
+
+ transition(from = KeyguardState.DREAMING, to = KeyguardState.GLANCEABLE_HUB)
+
+ verify(dreamManager, never()).startDream()
+ }
+
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
@Test
fun startDreamWhenTransitioningToHub() =
testScope.runTest {
+ underTest.start()
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setDreaming(false)
powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
@@ -100,6 +129,7 @@ class CommunalDreamStartableTest : SysuiTestCase() {
@EnableFlags(Flags.FLAG_RESTART_DREAM_ON_UNOCCLUDE)
fun restartDreamingWhenTransitioningFromDreamingToOccludedToDreaming() =
testScope.runTest {
+ underTest.start()
keyguardRepository.setDreaming(false)
powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true)
@@ -122,9 +152,11 @@ class CommunalDreamStartableTest : SysuiTestCase() {
verify(dreamManager).startDream()
}
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
@Test
fun shouldNotStartDreamWhenIneligibleToDream() =
testScope.runTest {
+ underTest.start()
keyguardRepository.setDreaming(false)
powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
// Not eligible to dream
@@ -134,9 +166,11 @@ class CommunalDreamStartableTest : SysuiTestCase() {
verify(dreamManager, never()).startDream()
}
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
@Test
fun shouldNotStartDreamIfAlreadyDreaming() =
testScope.runTest {
+ underTest.start()
keyguardRepository.setDreaming(true)
powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true)
@@ -145,9 +179,11 @@ class CommunalDreamStartableTest : SysuiTestCase() {
verify(dreamManager, never()).startDream()
}
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
@Test
fun shouldNotStartDreamForInvalidTransition() =
testScope.runTest {
+ underTest.start()
keyguardRepository.setDreaming(true)
powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true)
@@ -160,9 +196,11 @@ class CommunalDreamStartableTest : SysuiTestCase() {
}
}
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
@Test
fun shouldNotStartDreamWhenLaunchingWidget() =
testScope.runTest {
+ underTest.start()
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setDreaming(false)
powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
@@ -175,9 +213,11 @@ class CommunalDreamStartableTest : SysuiTestCase() {
verify(dreamManager, never()).startDream()
}
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
@Test
fun shouldNotStartDreamWhenOccluded() =
testScope.runTest {
+ underTest.start()
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setDreaming(false)
powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
@@ -194,8 +234,16 @@ class CommunalDreamStartableTest : SysuiTestCase() {
kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
from = from,
to = to,
- testScope = this
+ testScope = this,
)
runCurrent()
}
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf(FLAG_GLANCEABLE_HUB_V2)
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index 5921e9479bd9..0df584ff4dc1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -58,6 +58,7 @@ import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
+import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled
import com.android.systemui.communal.shared.log.CommunalUiEvent
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.complication.ComplicationHostViewController
@@ -747,7 +748,7 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
@Test
@EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_COMMUNAL_HUB)
- @DisableFlags(FLAG_SCENE_CONTAINER)
+ @DisableFlags(FLAG_SCENE_CONTAINER, FLAG_GLANCEABLE_HUB_V2)
@kotlin.Throws(RemoteException::class)
fun testTransitionToGlanceableHub() =
testScope.runTest {
@@ -774,6 +775,7 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
@Test
@EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_SCENE_CONTAINER, FLAG_COMMUNAL_HUB)
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
@kotlin.Throws(RemoteException::class)
fun testTransitionToGlanceableHub_sceneContainer() =
testScope.runTest {
@@ -802,7 +804,29 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
}
@Test
+ @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2)
+ @Throws(RemoteException::class)
+ fun testRedirect_v2Enabled_notTriggered() =
+ testScope.runTest {
+ kosmos.setCommunalV2Enabled(true)
+ // Inform the overlay service of dream starting. Do not show dream complications.
+ client.startDream(
+ mWindowParams,
+ mDreamOverlayCallback,
+ DREAM_COMPONENT,
+ false /*isPreview*/,
+ false, /*shouldShowComplication*/
+ )
+ // Set communal available, verify that onRedirectWake is never called.
+ kosmos.setCommunalAvailable(true)
+ mMainExecutor.runAllReady()
+ runCurrent()
+ verify(mDreamOverlayCallback, never()).onRedirectWake(any())
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_COMMUNAL_HUB)
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
@Throws(RemoteException::class)
fun testRedirectExit() =
testScope.runTest {
@@ -1347,7 +1371,11 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
@JvmStatic
@Parameters(name = "{0}")
fun getParams(): List<FlagsParameterization> {
- return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_HUB).andSceneContainer()
+ return FlagsParameterization.allCombinationsOf(
+ FLAG_COMMUNAL_HUB,
+ FLAG_GLANCEABLE_HUB_V2,
+ )
+ .andSceneContainer()
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
index 1bd541e1088a..6dc7c971baef 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
@@ -91,13 +91,19 @@ constructor(
.launchIn(bgScope)
}
- // Restart the dream underneath the hub in order to support the ability to swipe
- // away the hub to enter the dream.
- startDream
- .sampleFilter(powerInteractor.isAwake) { isAwake ->
- !glanceableHubAllowKeyguardWhenDreaming() && dreamManager.canStartDreaming(isAwake)
- }
- .onEach { dreamManager.startDream() }
- .launchIn(bgScope)
+ // With hub v2, we no longer need to keep the dream running underneath the hub as there is
+ // no more swipe between the hub and dream. We can just start the dream on-demand when the
+ // user presses the dream coin.
+ if (!communalSettingsInteractor.isV2FlagEnabled()) {
+ // Restart the dream underneath the hub in order to support the ability to swipe away
+ // the hub to enter the dream.
+ startDream
+ .sampleFilter(powerInteractor.isAwake) { isAwake ->
+ !glanceableHubAllowKeyguardWhenDreaming() &&
+ dreamManager.canStartDreaming(isAwake)
+ }
+ .onEach { dreamManager.startDream() }
+ .launchIn(bgScope)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 0b2b3687c121..a56a63c0b104 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -562,6 +562,13 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
return;
}
+ if (mCommunalSettingsInteractor.isV2FlagEnabled()) {
+ // Dream wake redirect is not needed in V2 as we do not need to keep the dream awake
+ // underneath the hub anymore as there is no more swipe between the dream and hub. SysUI
+ // will automatically transition to the hub when the dream wakes.
+ return;
+ }
+
redirectWake(mCommunalAvailable && !glanceableHubAllowKeyguardWhenDreaming());
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
index d9e55f89cda5..d8e2dabde8a9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyboard.shortcut.ui.composable
+import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -27,6 +28,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
@@ -211,19 +213,19 @@ private fun DialogButtons(
shape = RoundedCornerShape(50.dp),
onClick = onCancel,
color = Color.Transparent,
- width = 80.dp,
+ modifier = Modifier.heightIn(40.dp),
contentColor = MaterialTheme.colorScheme.primary,
text = stringResource(R.string.shortcut_helper_customize_dialog_cancel_button_label),
+ border = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.outlineVariant)
)
Spacer(modifier = Modifier.width(8.dp))
ShortcutHelperButton(
- modifier =
- Modifier.focusRequester(focusRequester).focusProperties {
- canFocus = true
- }, // enable focus on touch/click mode
+ modifier = Modifier
+ .heightIn(40.dp)
+ .focusRequester(focusRequester)
+ .focusProperties { canFocus = true }, // enable focus on touch/click mode
onClick = onConfirm,
color = MaterialTheme.colorScheme.primary,
- width = 116.dp,
contentColor = MaterialTheme.colorScheme.onPrimary,
text = confirmButtonText,
enabled = isConfirmButtonEnabled,
@@ -413,8 +415,7 @@ private fun PromptShortcutModifier(
private fun ActionKeyContainer(defaultModifierKey: ShortcutKey.Icon.ResIdIcon) {
Row(
modifier =
- Modifier.height(48.dp)
- .width(105.dp)
+ Modifier.sizeIn(minWidth = 105.dp, minHeight = 48.dp)
.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(16.dp),
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 bf60c9a52417..0054dd772659 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
@@ -1,1109 +1 @@
-/*
- * 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.keyboard.shortcut.ui.composable
-
-import android.graphics.drawable.Icon
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-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
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.FlowRow
-import androidx.compose.foundation.layout.FlowRowScope
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.OpenInNew
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.DeleteOutline
-import androidx.compose.material.icons.filled.ExpandMore
-import androidx.compose.material.icons.filled.Refresh
-import androidx.compose.material.icons.filled.Search
-import androidx.compose.material.icons.filled.Tune
-import androidx.compose.material3.CenterAlignedTopAppBar
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.Icon
-import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.NavigationDrawerItemColors
-import androidx.compose.material3.NavigationDrawerItemDefaults
-import androidx.compose.material3.SearchBar
-import androidx.compose.material3.SearchBarDefaults
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusDirection
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.onKeyEvent
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.hideFromAccessibility
-import androidx.compose.ui.semantics.isTraversalGroup
-import androidx.compose.ui.semantics.role
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.style.Hyphens
-import androidx.compose.ui.text.withStyle
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.compose.ui.util.fastFirstOrNull
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastForEachIndexed
-import com.android.compose.modifiers.thenIf
-import com.android.compose.ui.graphics.painter.rememberDrawablePainter
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
-import com.android.systemui.keyboard.shortcut.ui.model.IconSource
-import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCategoryUi
-import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
-import com.android.systemui.res.R
-import kotlinx.coroutines.delay
-import com.android.systemui.keyboard.shortcut.shared.model.Shortcut as ShortcutModel
-
-@Composable
-fun ShortcutHelper(
- onSearchQueryChanged: (String) -> Unit,
- onKeyboardSettingsClicked: () -> Unit,
- modifier: Modifier = Modifier,
- shortcutsUiState: ShortcutsUiState,
- useSinglePane: @Composable () -> Boolean = { shouldUseSinglePane() },
- onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
-) {
- when (shortcutsUiState) {
- is ShortcutsUiState.Active -> {
- ActiveShortcutHelper(
- shortcutsUiState,
- useSinglePane,
- onSearchQueryChanged,
- modifier,
- onKeyboardSettingsClicked,
- onCustomizationRequested,
- )
- }
-
- else -> {
- // No-op for now.
- }
- }
-}
-
-@Composable
-private fun ActiveShortcutHelper(
- shortcutsUiState: ShortcutsUiState.Active,
- useSinglePane: @Composable () -> Boolean,
- onSearchQueryChanged: (String) -> Unit,
- modifier: Modifier,
- onKeyboardSettingsClicked: () -> Unit,
- onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
-) {
- var selectedCategoryType by
- remember(shortcutsUiState.defaultSelectedCategory) {
- mutableStateOf(shortcutsUiState.defaultSelectedCategory)
- }
- if (useSinglePane()) {
- ShortcutHelperSinglePane(
- shortcutsUiState.searchQuery,
- onSearchQueryChanged,
- shortcutsUiState.shortcutCategories,
- selectedCategoryType,
- onCategorySelected = { selectedCategoryType = it },
- onKeyboardSettingsClicked,
- modifier,
- )
- } else {
- ShortcutHelperTwoPane(
- shortcutsUiState.searchQuery,
- onSearchQueryChanged,
- modifier,
- shortcutsUiState.shortcutCategories,
- selectedCategoryType,
- onCategorySelected = { selectedCategoryType = it },
- onKeyboardSettingsClicked,
- shortcutsUiState.isShortcutCustomizerFlagEnabled,
- onCustomizationRequested,
- shortcutsUiState.shouldShowResetButton,
- )
- }
-}
-
-@Composable private fun shouldUseSinglePane() = hasCompactWindowSize()
-
-@Composable
-private fun ShortcutHelperSinglePane(
- searchQuery: String,
- onSearchQueryChanged: (String) -> Unit,
- categories: List<ShortcutCategoryUi>,
- selectedCategoryType: ShortcutCategoryType?,
- onCategorySelected: (ShortcutCategoryType?) -> Unit,
- onKeyboardSettingsClicked: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- Column(
- modifier =
- modifier
- .fillMaxSize()
- .verticalScroll(rememberScrollState())
- .padding(start = 16.dp, end = 16.dp, top = 26.dp)
- ) {
- TitleBar()
- Spacer(modifier = Modifier.height(6.dp))
- ShortcutsSearchBar(onSearchQueryChanged)
- Spacer(modifier = Modifier.height(16.dp))
- if (categories.isEmpty()) {
- Box(modifier = Modifier.weight(1f)) {
- NoSearchResultsText(horizontalPadding = 16.dp, fillHeight = true)
- }
- } else {
- CategoriesPanelSinglePane(
- searchQuery,
- categories,
- selectedCategoryType,
- onCategorySelected,
- )
- Spacer(modifier = Modifier.weight(1f))
- }
- KeyboardSettings(
- horizontalPadding = 16.dp,
- verticalPadding = 32.dp,
- onClick = onKeyboardSettingsClicked,
- )
- }
-}
-
-@Composable
-private fun CategoriesPanelSinglePane(
- searchQuery: String,
- categories: List<ShortcutCategoryUi>,
- selectedCategoryType: ShortcutCategoryType?,
- onCategorySelected: (ShortcutCategoryType?) -> Unit,
-) {
- Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
- categories.fastForEachIndexed { index, category ->
- val isExpanded = selectedCategoryType == category.type
- val itemShape =
- if (categories.size == 1) {
- ShortcutHelper.Shapes.singlePaneSingleCategory
- } else if (index == 0) {
- ShortcutHelper.Shapes.singlePaneFirstCategory
- } else if (index == categories.lastIndex) {
- ShortcutHelper.Shapes.singlePaneLastCategory
- } else {
- ShortcutHelper.Shapes.singlePaneCategory
- }
- CategoryItemSinglePane(
- searchQuery = searchQuery,
- category = category,
- isExpanded = isExpanded,
- onClick = {
- onCategorySelected(
- if (isExpanded) {
- null
- } else {
- category.type
- }
- )
- },
- shape = itemShape,
- )
- }
- }
-}
-
-@Composable
-private fun CategoryItemSinglePane(
- searchQuery: String,
- category: ShortcutCategoryUi,
- isExpanded: Boolean,
- onClick: () -> Unit,
- shape: Shape,
-) {
- Surface(color = MaterialTheme.colorScheme.surfaceBright, shape = shape, onClick = onClick) {
- Column {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp).padding(horizontal = 16.dp),
- ) {
- ShortcutCategoryIcon(modifier = Modifier.size(24.dp), source = category.iconSource)
- Spacer(modifier = Modifier.width(16.dp))
- Text(category.label)
- Spacer(modifier = Modifier.weight(1f))
- RotatingExpandCollapseIcon(isExpanded)
- }
- AnimatedVisibility(visible = isExpanded) {
- ShortcutCategoryDetailsSinglePane(searchQuery, category)
- }
- }
- }
-}
-
-@Composable
-fun ShortcutCategoryIcon(
- source: IconSource,
- modifier: Modifier = Modifier,
- contentDescription: String? = null,
- tint: Color = LocalContentColor.current,
-) {
- if (source.imageVector != null) {
- Icon(source.imageVector, contentDescription, modifier, tint)
- } else if (source.painter != null) {
- Image(source.painter, contentDescription, modifier)
- }
-}
-
-@Composable
-private fun RotatingExpandCollapseIcon(isExpanded: Boolean) {
- val expandIconRotationDegrees by
- animateFloatAsState(
- targetValue =
- if (isExpanded) {
- 180f
- } else {
- 0f
- },
- label = "Expand icon rotation animation",
- )
- Icon(
- modifier =
- Modifier.background(
- color = MaterialTheme.colorScheme.surfaceContainerHigh,
- shape = CircleShape,
- )
- .graphicsLayer { rotationZ = expandIconRotationDegrees },
- imageVector = Icons.Default.ExpandMore,
- contentDescription =
- if (isExpanded) {
- stringResource(R.string.shortcut_helper_content_description_collapse_icon)
- } else {
- stringResource(R.string.shortcut_helper_content_description_expand_icon)
- },
- tint = MaterialTheme.colorScheme.onSurface,
- )
-}
-
-@Composable
-private fun ShortcutCategoryDetailsSinglePane(searchQuery: String, category: ShortcutCategoryUi) {
- Column(Modifier.padding(horizontal = 16.dp)) {
- category.subCategories.fastForEach { subCategory ->
- ShortcutSubCategorySinglePane(searchQuery, subCategory)
- }
- }
-}
-
-@Composable
-private fun ShortcutSubCategorySinglePane(searchQuery: String, subCategory: ShortcutSubCategory) {
- // This @Composable is expected to be in a Column.
- SubCategoryTitle(subCategory.label)
- subCategory.shortcuts.fastForEachIndexed { index, shortcut ->
- if (index > 0) {
- HorizontalDivider(color = MaterialTheme.colorScheme.surfaceContainerHigh)
- }
- Shortcut(Modifier.padding(vertical = 24.dp), searchQuery, shortcut)
- }
-}
-
-@Composable
-private fun ShortcutHelperTwoPane(
- searchQuery: String,
- onSearchQueryChanged: (String) -> Unit,
- modifier: Modifier = Modifier,
- categories: List<ShortcutCategoryUi>,
- selectedCategoryType: ShortcutCategoryType?,
- onCategorySelected: (ShortcutCategoryType?) -> Unit,
- onKeyboardSettingsClicked: () -> Unit,
- isShortcutCustomizerFlagEnabled: Boolean,
- onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
- shouldShowResetButton: Boolean,
-) {
- val selectedCategory = categories.fastFirstOrNull { it.type == selectedCategoryType }
- var isCustomizing by remember { mutableStateOf(false) }
-
- Column(modifier = modifier.fillMaxSize().padding(horizontal = 24.dp)) {
- Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- // Keep title centered whether customize button is visible or not.
- Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
- Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
- TitleBar(isCustomizing)
- }
- if (isShortcutCustomizerFlagEnabled) {
- CustomizationButtonsContainer(
- isCustomizing = isCustomizing,
- onToggleCustomizationMode = { isCustomizing = !isCustomizing },
- onReset = {
- onCustomizationRequested(ShortcutCustomizationRequestInfo.Reset)
- },
- shouldShowResetButton = shouldShowResetButton,
- )
- } else {
- Spacer(modifier = Modifier.width(if (isCustomizing) 69.dp else 133.dp))
- }
- }
- }
- Spacer(modifier = Modifier.height(12.dp))
- Row(Modifier.fillMaxWidth()) {
- StartSidePanel(
- onSearchQueryChanged = onSearchQueryChanged,
- modifier = Modifier.width(240.dp).semantics { isTraversalGroup = true },
- categories = categories,
- onKeyboardSettingsClicked = onKeyboardSettingsClicked,
- selectedCategory = selectedCategoryType,
- onCategoryClicked = { onCategorySelected(it.type) },
- )
- Spacer(modifier = Modifier.width(24.dp))
- EndSidePanel(
- searchQuery,
- Modifier.fillMaxSize().padding(top = 8.dp).semantics { isTraversalGroup = true },
- selectedCategory,
- isCustomizing = isCustomizing,
- onCustomizationRequested = onCustomizationRequested,
- )
- }
- }
-}
-
-@Composable
-private fun CustomizationButtonsContainer(
- isCustomizing: Boolean,
- shouldShowResetButton: Boolean,
- onToggleCustomizationMode: () -> Unit,
- onReset: () -> Unit,
-) {
- Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- if (isCustomizing) {
- if (shouldShowResetButton) {
- ResetButton(onClick = onReset)
- }
- DoneButton(onClick = onToggleCustomizationMode)
- } else {
- CustomizeButton(onClick = onToggleCustomizationMode)
- }
- }
-}
-
-@Composable
-private fun ResetButton(onClick: () -> Unit) {
- ShortcutHelperButton(
- onClick = onClick,
- color = Color.Transparent,
- width = 99.dp,
- iconSource = IconSource(imageVector = Icons.Default.Refresh),
- text = stringResource(id = R.string.shortcut_helper_reset_button_text),
- contentColor = MaterialTheme.colorScheme.primary,
- border = BorderStroke(color = MaterialTheme.colorScheme.outlineVariant, width = 1.dp),
- )
-}
-
-@Composable
-private fun CustomizeButton(onClick: () -> Unit) {
- ShortcutHelperButton(
- onClick = onClick,
- color = MaterialTheme.colorScheme.secondaryContainer,
- width = 133.dp,
- iconSource = IconSource(imageVector = Icons.Default.Tune),
- text = stringResource(id = R.string.shortcut_helper_customize_button_text),
- contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
- )
-}
-
-@Composable
-private fun DoneButton(onClick: () -> Unit) {
- ShortcutHelperButton(
- onClick = onClick,
- color = MaterialTheme.colorScheme.primary,
- width = 69.dp,
- text = stringResource(R.string.shortcut_helper_done_button_text),
- contentColor = MaterialTheme.colorScheme.onPrimary,
- )
-}
-
-@Composable
-private fun EndSidePanel(
- searchQuery: String,
- modifier: Modifier,
- category: ShortcutCategoryUi?,
- isCustomizing: Boolean,
- onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
-) {
- val listState = rememberLazyListState()
- LaunchedEffect(key1 = category) { if (category != null) listState.animateScrollToItem(0) }
- if (category == null) {
- NoSearchResultsText(horizontalPadding = 24.dp, fillHeight = false)
- return
- }
- LazyColumn(modifier = modifier, state = listState) {
- items(category.subCategories) { subcategory ->
- SubCategoryContainerDualPane(
- searchQuery = searchQuery,
- subCategory = subcategory,
- isCustomizing = isCustomizing and category.type.includeInCustomization,
- onCustomizationRequested = { requestInfo ->
- when (requestInfo) {
- is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add ->
- onCustomizationRequested(requestInfo.copy(categoryType = category.type))
-
- is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete ->
- onCustomizationRequested(requestInfo.copy(categoryType = category.type))
-
- ShortcutCustomizationRequestInfo.Reset ->
- onCustomizationRequested(requestInfo)
- }
- },
- )
- Spacer(modifier = Modifier.height(8.dp))
- }
- }
-}
-
-@Composable
-private fun NoSearchResultsText(horizontalPadding: Dp, fillHeight: Boolean) {
- var modifier = Modifier.fillMaxWidth()
- if (fillHeight) {
- modifier = modifier.fillMaxHeight()
- }
- Text(
- stringResource(R.string.shortcut_helper_no_search_results),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurface,
- modifier =
- modifier
- .padding(vertical = 8.dp)
- .background(MaterialTheme.colorScheme.surfaceBright, RoundedCornerShape(28.dp))
- .padding(horizontal = horizontalPadding, vertical = 24.dp),
- )
-}
-
-@Composable
-private fun SubCategoryContainerDualPane(
- searchQuery: String,
- subCategory: ShortcutSubCategory,
- isCustomizing: Boolean,
- onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit,
-) {
- Surface(
- modifier = Modifier.fillMaxWidth(),
- shape = RoundedCornerShape(28.dp),
- color = MaterialTheme.colorScheme.surfaceBright,
- ) {
- Column(Modifier.padding(16.dp)) {
- SubCategoryTitle(subCategory.label)
- Spacer(Modifier.height(8.dp))
- subCategory.shortcuts.fastForEachIndexed { index, shortcut ->
- if (index > 0) {
- HorizontalDivider(
- modifier = Modifier.padding(horizontal = 8.dp),
- color = MaterialTheme.colorScheme.surfaceContainerHigh,
- )
- }
- Shortcut(
- modifier = Modifier.padding(vertical = 8.dp),
- searchQuery = searchQuery,
- shortcut = shortcut,
- isCustomizing = isCustomizing && shortcut.isCustomizable,
- onCustomizationRequested = { requestInfo ->
- when (requestInfo) {
- is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add ->
- onCustomizationRequested(
- requestInfo.copy(subCategoryLabel = subCategory.label)
- )
-
- is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete ->
- onCustomizationRequested(
- requestInfo.copy(subCategoryLabel = subCategory.label)
- )
-
- ShortcutCustomizationRequestInfo.Reset ->
- onCustomizationRequested(requestInfo)
- }
- },
- )
- }
- }
- }
-}
-
-@Composable
-private fun SubCategoryTitle(title: String) {
- Text(
- title,
- style = MaterialTheme.typography.titleSmall,
- color = MaterialTheme.colorScheme.primary,
- )
-}
-
-@Composable
-private fun Shortcut(
- modifier: Modifier,
- searchQuery: String,
- shortcut: ShortcutModel,
- isCustomizing: Boolean = false,
- onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
-) {
- val interactionSource = remember { MutableInteractionSource() }
- val isFocused by interactionSource.collectIsFocusedAsState()
- val focusColor = MaterialTheme.colorScheme.secondary
- Row(
- modifier
- .thenIf(isFocused) {
- Modifier.border(width = 3.dp, color = focusColor, shape = RoundedCornerShape(16.dp))
- }
- .focusable(interactionSource = interactionSource)
- .padding(8.dp)
- .semantics(mergeDescendants = true) { contentDescription = shortcut.contentDescription }
- ) {
- Row(
- modifier =
- Modifier.width(128.dp).align(Alignment.CenterVertically).weight(0.333f).semantics {
- hideFromAccessibility()
- },
- horizontalArrangement = Arrangement.spacedBy(16.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- if (shortcut.icon != null) {
- ShortcutIcon(
- shortcut.icon,
- modifier = Modifier.size(24.dp).semantics { hideFromAccessibility() },
- )
- }
- ShortcutDescriptionText(
- searchQuery = searchQuery,
- shortcut = shortcut,
- modifier = Modifier.semantics { hideFromAccessibility() },
- )
- }
- Spacer(modifier = Modifier.width(24.dp).semantics { hideFromAccessibility() })
- ShortcutKeyCombinations(
- modifier = Modifier.weight(.666f).semantics { hideFromAccessibility() },
- shortcut = shortcut,
- isCustomizing = isCustomizing,
- onAddShortcutRequested = {
- onCustomizationRequested(
- ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add(
- label = shortcut.label,
- shortcutCommand = shortcut.commands.first(),
- )
- )
- },
- onDeleteShortcutRequested = {
- onCustomizationRequested(
- ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete(
- label = shortcut.label,
- shortcutCommand = shortcut.commands.first(),
- )
- )
- },
- )
- }
-}
-
-@Composable
-fun ShortcutIcon(
- icon: ShortcutIcon,
- modifier: Modifier = Modifier,
- contentDescription: String? = null,
-) {
- val context = LocalContext.current
- val drawable =
- remember(icon.packageName, icon.resourceId) {
- Icon.createWithResource(icon.packageName, icon.resourceId).loadDrawable(context)
- } ?: return
- Image(
- painter = rememberDrawablePainter(drawable),
- contentDescription = contentDescription,
- modifier = modifier,
- )
-}
-
-@OptIn(ExperimentalLayoutApi::class)
-@Composable
-private fun ShortcutKeyCombinations(
- modifier: Modifier = Modifier,
- shortcut: ShortcutModel,
- isCustomizing: Boolean = false,
- onAddShortcutRequested: () -> Unit = {},
- onDeleteShortcutRequested: () -> Unit = {},
-) {
- FlowRow(
- modifier = modifier,
- verticalArrangement = Arrangement.spacedBy(8.dp),
- itemVerticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.End,
- ) {
- shortcut.commands.forEachIndexed { index, command ->
- if (index > 0) {
- ShortcutOrSeparator(spacing = 16.dp)
- }
- ShortcutCommandContainer(showBackground = command.isCustom) { ShortcutCommand(command) }
- }
- if (isCustomizing) {
- Spacer(modifier = Modifier.width(16.dp))
- if (shortcut.containsCustomShortcutCommands) {
- DeleteShortcutButton(onDeleteShortcutRequested)
- } else {
- AddShortcutButton(onAddShortcutRequested)
- }
- }
- }
-}
-
-@Composable
-private fun AddShortcutButton(onClick: () -> Unit) {
- ShortcutHelperButton(
- modifier =
- Modifier.border(
- width = 1.dp,
- color = MaterialTheme.colorScheme.outline,
- shape = CircleShape,
- ),
- onClick = onClick,
- color = Color.Transparent,
- width = 32.dp,
- height = 32.dp,
- iconSource = IconSource(imageVector = Icons.Default.Add),
- contentColor = MaterialTheme.colorScheme.primary,
- contentPaddingVertical = 0.dp,
- contentPaddingHorizontal = 0.dp,
- contentDescription = stringResource(R.string.shortcut_helper_add_shortcut_button_label),
- )
-}
-
-@Composable
-private fun DeleteShortcutButton(onClick: () -> Unit) {
- ShortcutHelperButton(
- modifier =
- Modifier.border(
- width = 1.dp,
- color = MaterialTheme.colorScheme.outline,
- shape = CircleShape,
- ),
- onClick = onClick,
- color = Color.Transparent,
- width = 32.dp,
- height = 32.dp,
- iconSource = IconSource(imageVector = Icons.Default.DeleteOutline),
- contentColor = MaterialTheme.colorScheme.primary,
- contentPaddingVertical = 0.dp,
- contentPaddingHorizontal = 0.dp,
- contentDescription = stringResource(R.string.shortcut_helper_delete_shortcut_button_label),
- )
-}
-
-@Composable
-private fun ShortcutCommandContainer(showBackground: Boolean, content: @Composable () -> Unit) {
- if (showBackground) {
- Box(
- modifier =
- Modifier.wrapContentSize()
- .background(
- color = MaterialTheme.colorScheme.outlineVariant,
- shape = RoundedCornerShape(16.dp),
- )
- .padding(4.dp)
- ) {
- content()
- }
- } else {
- content()
- }
-}
-
-@Composable
-private fun ShortcutCommand(command: ShortcutCommand) {
- Row {
- command.keys.forEachIndexed { keyIndex, key ->
- if (keyIndex > 0) {
- Spacer(Modifier.width(4.dp))
- }
- ShortcutKeyContainer {
- if (key is ShortcutKey.Text) {
- ShortcutTextKey(key)
- } else if (key is ShortcutKey.Icon) {
- ShortcutIconKey(key)
- }
- }
- }
- }
-}
-
-@Composable
-private fun ShortcutKeyContainer(shortcutKeyContent: @Composable BoxScope.() -> Unit) {
- Box(
- modifier =
- Modifier.height(36.dp)
- .background(
- color = MaterialTheme.colorScheme.surfaceContainer,
- shape = RoundedCornerShape(12.dp),
- )
- ) {
- shortcutKeyContent()
- }
-}
-
-@Composable
-private fun BoxScope.ShortcutTextKey(key: ShortcutKey.Text) {
- Text(
- text = key.value,
- modifier =
- Modifier.align(Alignment.Center).padding(horizontal = 12.dp).semantics {
- hideFromAccessibility()
- },
- style = MaterialTheme.typography.titleSmall,
- )
-}
-
-@Composable
-private fun BoxScope.ShortcutIconKey(key: ShortcutKey.Icon) {
- Icon(
- painter =
- when (key) {
- is ShortcutKey.Icon.ResIdIcon -> painterResource(key.drawableResId)
- is ShortcutKey.Icon.DrawableIcon -> rememberDrawablePainter(drawable = key.drawable)
- },
- contentDescription = null,
- modifier = Modifier.align(Alignment.Center).padding(6.dp),
- )
-}
-
-@OptIn(ExperimentalLayoutApi::class)
-@Composable
-private fun FlowRowScope.ShortcutOrSeparator(spacing: Dp) {
- Spacer(Modifier.width(spacing))
- Text(
- text = stringResource(R.string.shortcut_helper_key_combinations_or_separator),
- modifier = Modifier.align(Alignment.CenterVertically).semantics { hideFromAccessibility() },
- style = MaterialTheme.typography.titleSmall,
- )
- Spacer(Modifier.width(spacing))
-}
-
-@Composable
-private fun ShortcutDescriptionText(
- searchQuery: String,
- shortcut: ShortcutModel,
- modifier: Modifier = Modifier,
-) {
- Text(
- modifier = modifier,
- text = textWithHighlightedSearchQuery(shortcut.label, searchQuery),
- style = MaterialTheme.typography.titleSmall,
- color = MaterialTheme.colorScheme.onSurface,
- )
-}
-
-@Composable
-private fun textWithHighlightedSearchQuery(text: String, searchValue: String) =
- buildAnnotatedString {
- val searchIndex = text.lowercase().indexOf(searchValue.trim().lowercase())
- val postSearchIndex = searchIndex + searchValue.trim().length
-
- if (searchIndex > 0) {
- val preSearchText = text.substring(0, searchIndex)
- append(preSearchText)
- }
- if (searchIndex >= 0) {
- val searchText = text.substring(searchIndex, postSearchIndex)
- withStyle(style = SpanStyle(background = MaterialTheme.colorScheme.primaryContainer)) {
- append(searchText)
- }
- if (postSearchIndex < text.length) {
- val postSearchText = text.substring(postSearchIndex)
- append(postSearchText)
- }
- } else {
- append(text)
- }
- }
-
-@Composable
-private fun StartSidePanel(
- onSearchQueryChanged: (String) -> Unit,
- modifier: Modifier,
- categories: List<ShortcutCategoryUi>,
- onKeyboardSettingsClicked: () -> Unit,
- selectedCategory: ShortcutCategoryType?,
- onCategoryClicked: (ShortcutCategoryUi) -> Unit,
-) {
- CompositionLocalProvider(
- // Restrict system font scale increases up to a max so categories display correctly.
- LocalDensity provides
- Density(
- density = LocalDensity.current.density,
- fontScale = LocalDensity.current.fontScale.coerceIn(1f, 1.5f),
- )
- ) {
- Column(modifier) {
- ShortcutsSearchBar(onSearchQueryChanged)
- Spacer(modifier = Modifier.heightIn(8.dp))
- CategoriesPanelTwoPane(categories, selectedCategory, onCategoryClicked)
- Spacer(modifier = Modifier.weight(1f))
- KeyboardSettings(
- horizontalPadding = 24.dp,
- verticalPadding = 24.dp,
- onKeyboardSettingsClicked,
- )
- }
- }
-}
-
-@Composable
-private fun CategoriesPanelTwoPane(
- categories: List<ShortcutCategoryUi>,
- selectedCategory: ShortcutCategoryType?,
- onCategoryClicked: (ShortcutCategoryUi) -> Unit,
-) {
- Column {
- categories.fastForEach {
- CategoryItemTwoPane(
- label = it.label,
- iconSource = it.iconSource,
- selected = selectedCategory == it.type,
- onClick = { onCategoryClicked(it) },
- )
- }
- }
-}
-
-@Composable
-private fun CategoryItemTwoPane(
- label: String,
- iconSource: IconSource,
- selected: Boolean,
- onClick: () -> Unit,
- colors: NavigationDrawerItemColors =
- NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent),
-) {
- SelectableShortcutSurface(
- selected = selected,
- onClick = onClick,
- modifier = Modifier.semantics { role = Role.Tab }.heightIn(min = 64.dp).fillMaxWidth(),
- shape = RoundedCornerShape(28.dp),
- color = colors.containerColor(selected).value,
- interactionsConfig =
- InteractionsConfig(
- hoverOverlayColor = MaterialTheme.colorScheme.onSurface,
- hoverOverlayAlpha = 0.11f,
- pressedOverlayColor = MaterialTheme.colorScheme.onSurface,
- pressedOverlayAlpha = 0.15f,
- focusOutlineColor = MaterialTheme.colorScheme.secondary,
- focusOutlineStrokeWidth = 3.dp,
- focusOutlinePadding = 2.dp,
- surfaceCornerRadius = 28.dp,
- focusOutlineCornerRadius = 33.dp,
- ),
- ) {
- Row(Modifier.padding(horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically) {
- ShortcutCategoryIcon(
- modifier = Modifier.size(24.dp),
- source = iconSource,
- contentDescription = null,
- tint = colors.iconColor(selected).value,
- )
- Spacer(Modifier.width(12.dp))
- Box(Modifier.weight(1f)) {
- Text(
- fontSize = 18.sp,
- color = colors.textColor(selected).value,
- style = MaterialTheme.typography.titleSmall.copy(hyphens = Hyphens.Auto),
- text = label,
- )
- }
- }
- }
-}
-
-@Composable
-@OptIn(ExperimentalMaterial3Api::class)
-private fun TitleBar(isCustomizing: Boolean = false) {
- val text =
- if (isCustomizing) {
- stringResource(R.string.shortcut_helper_customize_mode_title)
- } else {
- stringResource(R.string.shortcut_helper_title)
- }
- CenterAlignedTopAppBar(
- colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color.Transparent),
- title = {
- Text(
- text = text,
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.headlineSmall,
- )
- },
- windowInsets = WindowInsets(top = 0.dp, bottom = 0.dp, left = 0.dp, right = 0.dp),
- expandedHeight = 64.dp,
- )
-}
-
-@Composable
-@OptIn(ExperimentalMaterial3Api::class)
-private fun ShortcutsSearchBar(onQueryChange: (String) -> Unit) {
- // Using an "internal query" to make sure the SearchBar is immediately updated, otherwise
- // the cursor moves to the wrong position sometimes, when waiting for the query to come back
- // from the ViewModel.
- var queryInternal by remember { mutableStateOf("") }
- val focusRequester = remember { FocusRequester() }
- val focusManager = LocalFocusManager.current
- LaunchedEffect(Unit) {
- // TODO(b/272065229): Added minor delay so TalkBack can take focus of search box by default,
- // remove when default a11y focus is fixed.
- delay(50)
- focusRequester.requestFocus()
- }
- SearchBar(
- modifier =
- Modifier.fillMaxWidth().focusRequester(focusRequester).onKeyEvent {
- if (it.key == Key.DirectionDown) {
- focusManager.moveFocus(FocusDirection.Down)
- return@onKeyEvent true
- } else {
- return@onKeyEvent false
- }
- },
- colors = SearchBarDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceBright),
- query = queryInternal,
- active = false,
- onActiveChange = {},
- onQueryChange = {
- queryInternal = it
- onQueryChange(it)
- },
- onSearch = {},
- leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
- placeholder = { Text(text = stringResource(R.string.shortcut_helper_search_placeholder)) },
- windowInsets = WindowInsets(top = 0.dp, bottom = 0.dp, left = 0.dp, right = 0.dp),
- content = {},
- )
-}
-
-@Composable
-private fun KeyboardSettings(horizontalPadding: Dp, verticalPadding: Dp, onClick: () -> Unit) {
- ClickableShortcutSurface(
- onClick = onClick,
- shape = RoundedCornerShape(24.dp),
- color = Color.Transparent,
- modifier =
- Modifier.semantics { role = Role.Button }.fillMaxWidth().padding(horizontal = 12.dp),
- interactionsConfig =
- InteractionsConfig(
- hoverOverlayColor = MaterialTheme.colorScheme.onSurface,
- hoverOverlayAlpha = 0.11f,
- pressedOverlayColor = MaterialTheme.colorScheme.onSurface,
- pressedOverlayAlpha = 0.15f,
- focusOutlineColor = MaterialTheme.colorScheme.secondary,
- focusOutlinePadding = 8.dp,
- focusOutlineStrokeWidth = 3.dp,
- surfaceCornerRadius = 24.dp,
- focusOutlineCornerRadius = 28.dp,
- hoverPadding = 8.dp,
- ),
- ) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text =
- stringResource(id = R.string.shortcut_helper_keyboard_settings_buttons_label),
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- fontSize = 16.sp,
- style = MaterialTheme.typography.titleSmall,
- )
- Spacer(modifier = Modifier.weight(1f))
- Icon(
- imageVector = Icons.AutoMirrored.Default.OpenInNew,
- contentDescription = null,
- tint = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.size(24.dp),
- )
- }
- }
-}
-
-object ShortcutHelper {
-
- object Shapes {
- val singlePaneFirstCategory =
- RoundedCornerShape(
- topStart = Dimensions.SinglePaneCategoryCornerRadius,
- topEnd = Dimensions.SinglePaneCategoryCornerRadius,
- )
- val singlePaneLastCategory =
- RoundedCornerShape(
- bottomStart = Dimensions.SinglePaneCategoryCornerRadius,
- bottomEnd = Dimensions.SinglePaneCategoryCornerRadius,
- )
- val singlePaneSingleCategory =
- RoundedCornerShape(size = Dimensions.SinglePaneCategoryCornerRadius)
- val singlePaneCategory = RectangleShape
- }
-
- object Dimensions {
- val SinglePaneCategoryCornerRadius = 28.dp
- }
-
- internal const val TAG = "ShortcutHelperUI"
-}
+/* * 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.keyboard.shortcut.ui.composable import android.graphics.drawable.Icon import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border 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 import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRowScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.DeleteOutline import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Tune import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationDrawerItemColors import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.isTraversalGroup import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.Hyphens import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.util.fastFirstOrNull import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachIndexed import com.android.compose.modifiers.thenIf import com.android.compose.ui.graphics.painter.rememberDrawablePainter import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory import com.android.systemui.keyboard.shortcut.ui.model.IconSource import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCategoryUi import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState import com.android.systemui.res.R import kotlinx.coroutines.delay import com.android.systemui.keyboard.shortcut.shared.model.Shortcut as ShortcutModel @Composable fun ShortcutHelper( onSearchQueryChanged: (String) -> Unit, onKeyboardSettingsClicked: () -> Unit, modifier: Modifier = Modifier, shortcutsUiState: ShortcutsUiState, useSinglePane: @Composable () -> Boolean = { shouldUseSinglePane() }, onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, ) { when (shortcutsUiState) { is ShortcutsUiState.Active -> { ActiveShortcutHelper( shortcutsUiState, useSinglePane, onSearchQueryChanged, modifier, onKeyboardSettingsClicked, onCustomizationRequested, ) } else -> { // No-op for now. } } } @Composable private fun ActiveShortcutHelper( shortcutsUiState: ShortcutsUiState.Active, useSinglePane: @Composable () -> Boolean, onSearchQueryChanged: (String) -> Unit, modifier: Modifier, onKeyboardSettingsClicked: () -> Unit, onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, ) { var selectedCategoryType by remember(shortcutsUiState.defaultSelectedCategory) { mutableStateOf(shortcutsUiState.defaultSelectedCategory) } if (useSinglePane()) { ShortcutHelperSinglePane( shortcutsUiState.searchQuery, onSearchQueryChanged, shortcutsUiState.shortcutCategories, selectedCategoryType, onCategorySelected = { selectedCategoryType = it }, onKeyboardSettingsClicked, modifier, ) } else { ShortcutHelperTwoPane( shortcutsUiState.searchQuery, onSearchQueryChanged, modifier, shortcutsUiState.shortcutCategories, selectedCategoryType, onCategorySelected = { selectedCategoryType = it }, onKeyboardSettingsClicked, shortcutsUiState.isShortcutCustomizerFlagEnabled, onCustomizationRequested, shortcutsUiState.shouldShowResetButton, ) } } @Composable private fun shouldUseSinglePane() = hasCompactWindowSize() @Composable private fun ShortcutHelperSinglePane( searchQuery: String, onSearchQueryChanged: (String) -> Unit, categories: List<ShortcutCategoryUi>, selectedCategoryType: ShortcutCategoryType?, onCategorySelected: (ShortcutCategoryType?) -> Unit, onKeyboardSettingsClicked: () -> Unit, modifier: Modifier = Modifier, ) { Column( modifier = modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .padding(start = 16.dp, end = 16.dp, top = 26.dp) ) { TitleBar() Spacer(modifier = Modifier.height(6.dp)) ShortcutsSearchBar(onSearchQueryChanged) Spacer(modifier = Modifier.height(16.dp)) if (categories.isEmpty()) { Box(modifier = Modifier.weight(1f)) { NoSearchResultsText(horizontalPadding = 16.dp, fillHeight = true) } } else { CategoriesPanelSinglePane( searchQuery, categories, selectedCategoryType, onCategorySelected, ) Spacer(modifier = Modifier.weight(1f)) } KeyboardSettings( horizontalPadding = 16.dp, verticalPadding = 32.dp, onClick = onKeyboardSettingsClicked, ) } } @Composable private fun CategoriesPanelSinglePane( searchQuery: String, categories: List<ShortcutCategoryUi>, selectedCategoryType: ShortcutCategoryType?, onCategorySelected: (ShortcutCategoryType?) -> Unit, ) { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { categories.fastForEachIndexed { index, category -> val isExpanded = selectedCategoryType == category.type val itemShape = if (categories.size == 1) { ShortcutHelper.Shapes.singlePaneSingleCategory } else if (index == 0) { ShortcutHelper.Shapes.singlePaneFirstCategory } else if (index == categories.lastIndex) { ShortcutHelper.Shapes.singlePaneLastCategory } else { ShortcutHelper.Shapes.singlePaneCategory } CategoryItemSinglePane( searchQuery = searchQuery, category = category, isExpanded = isExpanded, onClick = { onCategorySelected( if (isExpanded) { null } else { category.type } ) }, shape = itemShape, ) } } } @Composable private fun CategoryItemSinglePane( searchQuery: String, category: ShortcutCategoryUi, isExpanded: Boolean, onClick: () -> Unit, shape: Shape, ) { Surface(color = MaterialTheme.colorScheme.surfaceBright, shape = shape, onClick = onClick) { Column { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp).padding(horizontal = 16.dp), ) { ShortcutCategoryIcon(modifier = Modifier.size(24.dp), source = category.iconSource) Spacer(modifier = Modifier.width(16.dp)) Text(category.label) Spacer(modifier = Modifier.weight(1f)) RotatingExpandCollapseIcon(isExpanded) } AnimatedVisibility(visible = isExpanded) { ShortcutCategoryDetailsSinglePane(searchQuery, category) } } } } @Composable fun ShortcutCategoryIcon( source: IconSource, modifier: Modifier = Modifier, contentDescription: String? = null, tint: Color = LocalContentColor.current, ) { if (source.imageVector != null) { Icon(source.imageVector, contentDescription, modifier, tint) } else if (source.painter != null) { Image(source.painter, contentDescription, modifier) } } @Composable private fun RotatingExpandCollapseIcon(isExpanded: Boolean) { val expandIconRotationDegrees by animateFloatAsState( targetValue = if (isExpanded) { 180f } else { 0f }, label = "Expand icon rotation animation", ) Icon( modifier = Modifier.background( color = MaterialTheme.colorScheme.surfaceContainerHigh, shape = CircleShape, ) .graphicsLayer { rotationZ = expandIconRotationDegrees }, imageVector = Icons.Default.ExpandMore, contentDescription = if (isExpanded) { stringResource(R.string.shortcut_helper_content_description_collapse_icon) } else { stringResource(R.string.shortcut_helper_content_description_expand_icon) }, tint = MaterialTheme.colorScheme.onSurface, ) } @Composable private fun ShortcutCategoryDetailsSinglePane(searchQuery: String, category: ShortcutCategoryUi) { Column(Modifier.padding(horizontal = 16.dp)) { category.subCategories.fastForEach { subCategory -> ShortcutSubCategorySinglePane(searchQuery, subCategory) } } } @Composable private fun ShortcutSubCategorySinglePane(searchQuery: String, subCategory: ShortcutSubCategory) { // This @Composable is expected to be in a Column. SubCategoryTitle(subCategory.label) subCategory.shortcuts.fastForEachIndexed { index, shortcut -> if (index > 0) { HorizontalDivider(color = MaterialTheme.colorScheme.surfaceContainerHigh) } Shortcut(Modifier.padding(vertical = 24.dp), searchQuery, shortcut) } } @Composable private fun ShortcutHelperTwoPane( searchQuery: String, onSearchQueryChanged: (String) -> Unit, modifier: Modifier = Modifier, categories: List<ShortcutCategoryUi>, selectedCategoryType: ShortcutCategoryType?, onCategorySelected: (ShortcutCategoryType?) -> Unit, onKeyboardSettingsClicked: () -> Unit, isShortcutCustomizerFlagEnabled: Boolean, onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, shouldShowResetButton: Boolean, ) { val selectedCategory = categories.fastFirstOrNull { it.type == selectedCategoryType } var isCustomizing by remember { mutableStateOf(false) } Column(modifier = modifier.fillMaxSize().padding(horizontal = 24.dp)) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { // Keep title centered whether customize button is visible or not. Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { TitleBar(isCustomizing) } if (isShortcutCustomizerFlagEnabled) { CustomizationButtonsContainer( isCustomizing = isCustomizing, onToggleCustomizationMode = { isCustomizing = !isCustomizing }, onReset = { onCustomizationRequested(ShortcutCustomizationRequestInfo.Reset) }, shouldShowResetButton = shouldShowResetButton, ) } else { Spacer(modifier = Modifier.width(if (isCustomizing) 69.dp else 133.dp)) } } } Spacer(modifier = Modifier.height(12.dp)) Row(Modifier.fillMaxWidth()) { StartSidePanel( onSearchQueryChanged = onSearchQueryChanged, modifier = Modifier.width(240.dp).semantics { isTraversalGroup = true }, categories = categories, onKeyboardSettingsClicked = onKeyboardSettingsClicked, selectedCategory = selectedCategoryType, onCategoryClicked = { onCategorySelected(it.type) }, ) Spacer(modifier = Modifier.width(24.dp)) EndSidePanel( searchQuery, Modifier.fillMaxSize().padding(top = 8.dp).semantics { isTraversalGroup = true }, selectedCategory, isCustomizing = isCustomizing, onCustomizationRequested = onCustomizationRequested, ) } } } @Composable private fun CustomizationButtonsContainer( isCustomizing: Boolean, shouldShowResetButton: Boolean, onToggleCustomizationMode: () -> Unit, onReset: () -> Unit, ) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { if (isCustomizing) { if (shouldShowResetButton) { ResetButton(onClick = onReset) } DoneButton(onClick = onToggleCustomizationMode) } else { CustomizeButton(onClick = onToggleCustomizationMode) } } } @Composable private fun ResetButton(onClick: () -> Unit) { ShortcutHelperButton( modifier = Modifier.heightIn(40.dp), onClick = onClick, color = Color.Transparent, iconSource = IconSource(imageVector = Icons.Default.Refresh), text = stringResource(id = R.string.shortcut_helper_reset_button_text), contentColor = MaterialTheme.colorScheme.primary, border = BorderStroke(color = MaterialTheme.colorScheme.outlineVariant, width = 1.dp), ) } @Composable private fun CustomizeButton(onClick: () -> Unit) { ShortcutHelperButton( modifier = Modifier.heightIn(40.dp), onClick = onClick, color = MaterialTheme.colorScheme.secondaryContainer, iconSource = IconSource(imageVector = Icons.Default.Tune), text = stringResource(id = R.string.shortcut_helper_customize_button_text), contentColor = MaterialTheme.colorScheme.onSecondaryContainer, ) } @Composable private fun DoneButton(onClick: () -> Unit) { ShortcutHelperButton( modifier = Modifier.heightIn(40.dp), onClick = onClick, color = MaterialTheme.colorScheme.primary, text = stringResource(R.string.shortcut_helper_done_button_text), contentColor = MaterialTheme.colorScheme.onPrimary, ) } @Composable private fun EndSidePanel( searchQuery: String, modifier: Modifier, category: ShortcutCategoryUi?, isCustomizing: Boolean, onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, ) { val listState = rememberLazyListState() LaunchedEffect(key1 = category) { if (category != null) listState.animateScrollToItem(0) } if (category == null) { NoSearchResultsText(horizontalPadding = 24.dp, fillHeight = false) return } LazyColumn(modifier = modifier, state = listState) { items(category.subCategories) { subcategory -> SubCategoryContainerDualPane( searchQuery = searchQuery, subCategory = subcategory, isCustomizing = isCustomizing and category.type.includeInCustomization, onCustomizationRequested = { requestInfo -> when (requestInfo) { is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add -> onCustomizationRequested(requestInfo.copy(categoryType = category.type)) is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete -> onCustomizationRequested(requestInfo.copy(categoryType = category.type)) ShortcutCustomizationRequestInfo.Reset -> onCustomizationRequested(requestInfo) } }, ) Spacer(modifier = Modifier.height(8.dp)) } } } @Composable private fun NoSearchResultsText(horizontalPadding: Dp, fillHeight: Boolean) { var modifier = Modifier.fillMaxWidth() if (fillHeight) { modifier = modifier.fillMaxHeight() } Text( stringResource(R.string.shortcut_helper_no_search_results), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, modifier = modifier .padding(vertical = 8.dp) .background(MaterialTheme.colorScheme.surfaceBright, RoundedCornerShape(28.dp)) .padding(horizontal = horizontalPadding, vertical = 24.dp), ) } @Composable private fun SubCategoryContainerDualPane( searchQuery: String, subCategory: ShortcutSubCategory, isCustomizing: Boolean, onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit, ) { Surface( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(28.dp), color = MaterialTheme.colorScheme.surfaceBright, ) { Column(Modifier.padding(16.dp)) { SubCategoryTitle(subCategory.label) Spacer(Modifier.height(8.dp)) subCategory.shortcuts.fastForEachIndexed { index, shortcut -> if (index > 0) { HorizontalDivider( modifier = Modifier.padding(horizontal = 8.dp), color = MaterialTheme.colorScheme.surfaceContainerHigh, ) } Shortcut( modifier = Modifier.padding(vertical = 8.dp), searchQuery = searchQuery, shortcut = shortcut, isCustomizing = isCustomizing && shortcut.isCustomizable, onCustomizationRequested = { requestInfo -> when (requestInfo) { is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add -> onCustomizationRequested( requestInfo.copy(subCategoryLabel = subCategory.label) ) is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete -> onCustomizationRequested( requestInfo.copy(subCategoryLabel = subCategory.label) ) ShortcutCustomizationRequestInfo.Reset -> onCustomizationRequested(requestInfo) } }, ) } } } } @Composable private fun SubCategoryTitle(title: String) { Text( title, style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.primary, ) } @Composable private fun Shortcut( modifier: Modifier, searchQuery: String, shortcut: ShortcutModel, isCustomizing: Boolean = false, onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {}, ) { val interactionSource = remember { MutableInteractionSource() } val isFocused by interactionSource.collectIsFocusedAsState() val focusColor = MaterialTheme.colorScheme.secondary Row( modifier .thenIf(isFocused) { Modifier.border(width = 3.dp, color = focusColor, shape = RoundedCornerShape(16.dp)) } .focusable(interactionSource = interactionSource) .padding(8.dp) .semantics(mergeDescendants = true) { contentDescription = shortcut.contentDescription } ) { Row( modifier = Modifier.width(128.dp).align(Alignment.CenterVertically).weight(0.333f).semantics { hideFromAccessibility() }, horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, ) { if (shortcut.icon != null) { ShortcutIcon( shortcut.icon, modifier = Modifier.size(24.dp).semantics { hideFromAccessibility() }, ) } ShortcutDescriptionText( searchQuery = searchQuery, shortcut = shortcut, modifier = Modifier.semantics { hideFromAccessibility() }, ) } Spacer(modifier = Modifier.width(24.dp).semantics { hideFromAccessibility() }) ShortcutKeyCombinations( modifier = Modifier.weight(.666f).semantics { hideFromAccessibility() }, shortcut = shortcut, isCustomizing = isCustomizing, onAddShortcutRequested = { onCustomizationRequested( ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add( label = shortcut.label, shortcutCommand = shortcut.commands.first(), ) ) }, onDeleteShortcutRequested = { onCustomizationRequested( ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete( label = shortcut.label, shortcutCommand = shortcut.commands.first(), ) ) }, ) } } @Composable fun ShortcutIcon( icon: ShortcutIcon, modifier: Modifier = Modifier, contentDescription: String? = null, ) { val context = LocalContext.current val drawable = remember(icon.packageName, icon.resourceId) { Icon.createWithResource(icon.packageName, icon.resourceId).loadDrawable(context) } ?: return Image( painter = rememberDrawablePainter(drawable), contentDescription = contentDescription, modifier = modifier, ) } @OptIn(ExperimentalLayoutApi::class) @Composable private fun ShortcutKeyCombinations( modifier: Modifier = Modifier, shortcut: ShortcutModel, isCustomizing: Boolean = false, onAddShortcutRequested: () -> Unit = {}, onDeleteShortcutRequested: () -> Unit = {}, ) { FlowRow( modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp), itemVerticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End, ) { shortcut.commands.forEachIndexed { index, command -> if (index > 0) { ShortcutOrSeparator(spacing = 16.dp) } ShortcutCommandContainer(showBackground = command.isCustom) { ShortcutCommand(command) } } if (isCustomizing) { Spacer(modifier = Modifier.width(16.dp)) if (shortcut.containsCustomShortcutCommands) { DeleteShortcutButton(onDeleteShortcutRequested) } else { AddShortcutButton(onAddShortcutRequested) } } } } @Composable private fun AddShortcutButton(onClick: () -> Unit) { ShortcutHelperButton( modifier = Modifier.size(32.dp), onClick = onClick, color = Color.Transparent, iconSource = IconSource(imageVector = Icons.Default.Add), contentColor = MaterialTheme.colorScheme.primary, contentPaddingVertical = 0.dp, contentPaddingHorizontal = 0.dp, contentDescription = stringResource(R.string.shortcut_helper_add_shortcut_button_label), shape = CircleShape, border = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.outline) ) } @Composable private fun DeleteShortcutButton(onClick: () -> Unit) { ShortcutHelperButton( modifier = Modifier.size(32.dp), onClick = onClick, color = Color.Transparent, iconSource = IconSource(imageVector = Icons.Default.DeleteOutline), contentColor = MaterialTheme.colorScheme.primary, contentPaddingVertical = 0.dp, contentPaddingHorizontal = 0.dp, contentDescription = stringResource(R.string.shortcut_helper_delete_shortcut_button_label), shape = CircleShape, border = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.outline) ) } @Composable private fun ShortcutCommandContainer(showBackground: Boolean, content: @Composable () -> Unit) { if (showBackground) { Box( modifier = Modifier.wrapContentSize() .background( color = MaterialTheme.colorScheme.outlineVariant, shape = RoundedCornerShape(16.dp), ) .padding(4.dp) ) { content() } } else { content() } } @Composable private fun ShortcutCommand(command: ShortcutCommand) { Row { command.keys.forEachIndexed { keyIndex, key -> if (keyIndex > 0) { Spacer(Modifier.width(4.dp)) } ShortcutKeyContainer { if (key is ShortcutKey.Text) { ShortcutTextKey(key) } else if (key is ShortcutKey.Icon) { ShortcutIconKey(key) } } } } } @Composable private fun ShortcutKeyContainer(shortcutKeyContent: @Composable BoxScope.() -> Unit) { Box( modifier = Modifier.height(36.dp) .background( color = MaterialTheme.colorScheme.surfaceContainer, shape = RoundedCornerShape(12.dp), ) ) { shortcutKeyContent() } } @Composable private fun BoxScope.ShortcutTextKey(key: ShortcutKey.Text) { Text( text = key.value, modifier = Modifier.align(Alignment.Center).padding(horizontal = 12.dp).semantics { hideFromAccessibility() }, style = MaterialTheme.typography.titleSmall, ) } @Composable private fun BoxScope.ShortcutIconKey(key: ShortcutKey.Icon) { Icon( painter = when (key) { is ShortcutKey.Icon.ResIdIcon -> painterResource(key.drawableResId) is ShortcutKey.Icon.DrawableIcon -> rememberDrawablePainter(drawable = key.drawable) }, contentDescription = null, modifier = Modifier.align(Alignment.Center).padding(6.dp), ) } @OptIn(ExperimentalLayoutApi::class) @Composable private fun FlowRowScope.ShortcutOrSeparator(spacing: Dp) { Spacer(Modifier.width(spacing)) Text( text = stringResource(R.string.shortcut_helper_key_combinations_or_separator), modifier = Modifier.align(Alignment.CenterVertically).semantics { hideFromAccessibility() }, style = MaterialTheme.typography.titleSmall, ) Spacer(Modifier.width(spacing)) } @Composable private fun ShortcutDescriptionText( searchQuery: String, shortcut: ShortcutModel, modifier: Modifier = Modifier, ) { Text( modifier = modifier, text = textWithHighlightedSearchQuery(shortcut.label, searchQuery), style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.onSurface, ) } @Composable private fun textWithHighlightedSearchQuery(text: String, searchValue: String) = buildAnnotatedString { val searchIndex = text.lowercase().indexOf(searchValue.trim().lowercase()) val postSearchIndex = searchIndex + searchValue.trim().length if (searchIndex > 0) { val preSearchText = text.substring(0, searchIndex) append(preSearchText) } if (searchIndex >= 0) { val searchText = text.substring(searchIndex, postSearchIndex) withStyle(style = SpanStyle(background = MaterialTheme.colorScheme.primaryContainer)) { append(searchText) } if (postSearchIndex < text.length) { val postSearchText = text.substring(postSearchIndex) append(postSearchText) } } else { append(text) } } @Composable private fun StartSidePanel( onSearchQueryChanged: (String) -> Unit, modifier: Modifier, categories: List<ShortcutCategoryUi>, onKeyboardSettingsClicked: () -> Unit, selectedCategory: ShortcutCategoryType?, onCategoryClicked: (ShortcutCategoryUi) -> Unit, ) { CompositionLocalProvider( // Restrict system font scale increases up to a max so categories display correctly. LocalDensity provides Density( density = LocalDensity.current.density, fontScale = LocalDensity.current.fontScale.coerceIn(1f, 1.5f), ) ) { Column(modifier) { ShortcutsSearchBar(onSearchQueryChanged) Spacer(modifier = Modifier.heightIn(8.dp)) CategoriesPanelTwoPane(categories, selectedCategory, onCategoryClicked) Spacer(modifier = Modifier.weight(1f)) KeyboardSettings( horizontalPadding = 24.dp, verticalPadding = 24.dp, onKeyboardSettingsClicked, ) } } } @Composable private fun CategoriesPanelTwoPane( categories: List<ShortcutCategoryUi>, selectedCategory: ShortcutCategoryType?, onCategoryClicked: (ShortcutCategoryUi) -> Unit, ) { Column { categories.fastForEach { CategoryItemTwoPane( label = it.label, iconSource = it.iconSource, selected = selectedCategory == it.type, onClick = { onCategoryClicked(it) }, ) } } } @Composable private fun CategoryItemTwoPane( label: String, iconSource: IconSource, selected: Boolean, onClick: () -> Unit, colors: NavigationDrawerItemColors = NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent), ) { SelectableShortcutSurface( selected = selected, onClick = onClick, modifier = Modifier.semantics { role = Role.Tab }.heightIn(min = 64.dp).fillMaxWidth(), shape = RoundedCornerShape(28.dp), color = colors.containerColor(selected).value, interactionsConfig = InteractionsConfig( hoverOverlayColor = MaterialTheme.colorScheme.onSurface, hoverOverlayAlpha = 0.11f, pressedOverlayColor = MaterialTheme.colorScheme.onSurface, pressedOverlayAlpha = 0.15f, focusOutlineColor = MaterialTheme.colorScheme.secondary, focusOutlineStrokeWidth = 3.dp, focusOutlinePadding = 2.dp, surfaceCornerRadius = 28.dp, focusOutlineCornerRadius = 33.dp, ), ) { Row(Modifier.padding(horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically) { ShortcutCategoryIcon( modifier = Modifier.size(24.dp), source = iconSource, contentDescription = null, tint = colors.iconColor(selected).value, ) Spacer(Modifier.width(12.dp)) Box(Modifier.weight(1f)) { Text( fontSize = 18.sp, color = colors.textColor(selected).value, style = MaterialTheme.typography.titleSmall.copy(hyphens = Hyphens.Auto), text = label, ) } } } } @Composable @OptIn(ExperimentalMaterial3Api::class) private fun TitleBar(isCustomizing: Boolean = false) { val text = if (isCustomizing) { stringResource(R.string.shortcut_helper_customize_mode_title) } else { stringResource(R.string.shortcut_helper_title) } CenterAlignedTopAppBar( colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color.Transparent), title = { Text( text = text, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.headlineSmall, ) }, windowInsets = WindowInsets(top = 0.dp, bottom = 0.dp, left = 0.dp, right = 0.dp), expandedHeight = 64.dp, ) } @Composable @OptIn(ExperimentalMaterial3Api::class) private fun ShortcutsSearchBar(onQueryChange: (String) -> Unit) { // Using an "internal query" to make sure the SearchBar is immediately updated, otherwise // the cursor moves to the wrong position sometimes, when waiting for the query to come back // from the ViewModel. var queryInternal by remember { mutableStateOf("") } val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current LaunchedEffect(Unit) { // TODO(b/272065229): Added minor delay so TalkBack can take focus of search box by default, // remove when default a11y focus is fixed. delay(50) focusRequester.requestFocus() } SearchBar( modifier = Modifier.fillMaxWidth().focusRequester(focusRequester).onKeyEvent { if (it.key == Key.DirectionDown) { focusManager.moveFocus(FocusDirection.Down) return@onKeyEvent true } else { return@onKeyEvent false } }, colors = SearchBarDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceBright), query = queryInternal, active = false, onActiveChange = {}, onQueryChange = { queryInternal = it onQueryChange(it) }, onSearch = {}, leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, placeholder = { Text(text = stringResource(R.string.shortcut_helper_search_placeholder)) }, windowInsets = WindowInsets(top = 0.dp, bottom = 0.dp, left = 0.dp, right = 0.dp), content = {}, ) } @Composable private fun KeyboardSettings(horizontalPadding: Dp, verticalPadding: Dp, onClick: () -> Unit) { ClickableShortcutSurface( onClick = onClick, shape = RoundedCornerShape(24.dp), color = Color.Transparent, modifier = Modifier.semantics { role = Role.Button }.fillMaxWidth().padding(horizontal = 12.dp), interactionsConfig = InteractionsConfig( hoverOverlayColor = MaterialTheme.colorScheme.onSurface, hoverOverlayAlpha = 0.11f, pressedOverlayColor = MaterialTheme.colorScheme.onSurface, pressedOverlayAlpha = 0.15f, focusOutlineColor = MaterialTheme.colorScheme.secondary, focusOutlinePadding = 8.dp, focusOutlineStrokeWidth = 3.dp, surfaceCornerRadius = 24.dp, focusOutlineCornerRadius = 28.dp, hoverPadding = 8.dp, ), ) { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = stringResource(id = R.string.shortcut_helper_keyboard_settings_buttons_label), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 16.sp, style = MaterialTheme.typography.titleSmall, ) Spacer(modifier = Modifier.weight(1f)) Icon( imageVector = Icons.AutoMirrored.Default.OpenInNew, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(24.dp), ) } } } object ShortcutHelper { object Shapes { val singlePaneFirstCategory = RoundedCornerShape( topStart = Dimensions.SinglePaneCategoryCornerRadius, topEnd = Dimensions.SinglePaneCategoryCornerRadius, ) val singlePaneLastCategory = RoundedCornerShape( bottomStart = Dimensions.SinglePaneCategoryCornerRadius, bottomEnd = Dimensions.SinglePaneCategoryCornerRadius, ) val singlePaneSingleCategory = RoundedCornerShape(size = Dimensions.SinglePaneCategoryCornerRadius) val singlePaneCategory = RectangleShape } object Dimensions { val SinglePaneCategoryCornerRadius = 28.dp } internal const val TAG = "ShortcutHelperUI" } \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt
index 9a380f495176..981a55cfda5a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt
@@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -45,7 +44,6 @@ import androidx.compose.material3.LocalAbsoluteTonalElevation
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTonalElevationEnabled
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.material3.minimumInteractiveComponentSize
@@ -74,13 +72,14 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
-import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.modifiers.thenIf
import com.android.systemui.keyboard.shortcut.ui.model.IconSource
+import com.android.app.tracing.coroutines.launchTraced as launch
/**
* A selectable surface with no default focus/hover indications.
@@ -217,30 +216,37 @@ fun ClickableShortcutSurface(
*/
@Composable
fun ShortcutHelperButton(
- modifier: Modifier = Modifier,
onClick: () -> Unit,
- shape: Shape = RoundedCornerShape(360.dp),
+ contentColor: Color,
color: Color,
- width: Dp,
- height: Dp = 40.dp,
+ modifier: Modifier = Modifier,
+ shape: Shape = RoundedCornerShape(360.dp),
iconSource: IconSource = IconSource(),
text: String? = null,
- contentColor: Color,
contentPaddingHorizontal: Dp = 16.dp,
contentPaddingVertical: Dp = 10.dp,
enabled: Boolean = true,
border: BorderStroke? = null,
contentDescription: String? = null,
) {
- ShortcutHelperButtonSurface(
+ ClickableShortcutSurface(
onClick = onClick,
shape = shape,
- color = color,
- modifier = modifier,
- enabled = enabled,
- width = width,
- height = height,
+ color = color.getDimmedColorIfDisabled(enabled),
border = border,
+ modifier = modifier.semantics { role = Role.Button },
+ interactionsConfig = InteractionsConfig(
+ hoverOverlayColor = MaterialTheme.colorScheme.onSurface,
+ hoverOverlayAlpha = 0.11f,
+ pressedOverlayColor = MaterialTheme.colorScheme.onSurface,
+ pressedOverlayAlpha = 0.15f,
+ focusOutlineColor = MaterialTheme.colorScheme.secondary,
+ focusOutlineStrokeWidth = 3.dp,
+ focusOutlinePadding = 2.dp,
+ surfaceCornerRadius = 28.dp,
+ focusOutlineCornerRadius = 33.dp,
+ ),
+ enabled = enabled
) {
Row(
modifier =
@@ -251,76 +257,45 @@ fun ShortcutHelperButton(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
- if (iconSource.imageVector != null) {
- Icon(
- tint = contentColor,
- imageVector = iconSource.imageVector,
- contentDescription = contentDescription,
- modifier = Modifier.size(20.dp).wrapContentSize(Alignment.Center),
- )
- }
-
- if (iconSource.imageVector != null && text != null)
- Spacer(modifier = Modifier.weight(1f))
-
- if (text != null) {
- Text(
- text,
- color = contentColor,
- fontSize = 14.sp,
- style = MaterialTheme.typography.labelLarge,
- modifier = Modifier.wrapContentSize(Alignment.Center),
- )
- }
+ ShortcutHelperButtonContent(iconSource, contentColor, text, contentDescription)
}
}
}
@Composable
-private fun ShortcutHelperButtonSurface(
- onClick: () -> Unit,
- shape: Shape,
- color: Color,
- modifier: Modifier = Modifier,
- enabled: Boolean,
- width: Dp,
- height: Dp,
- border: BorderStroke?,
- content: @Composable () -> Unit,
+private fun ShortcutHelperButtonContent(
+ iconSource: IconSource,
+ contentColor: Color,
+ text: String?,
+ contentDescription: String?
) {
- if (enabled) {
- ClickableShortcutSurface(
- onClick = onClick,
- shape = shape,
- color = color,
- border = border,
- modifier = modifier.semantics { role = Role.Button }.width(width).height(height),
- interactionsConfig =
- InteractionsConfig(
- hoverOverlayColor = MaterialTheme.colorScheme.onSurface,
- hoverOverlayAlpha = 0.11f,
- pressedOverlayColor = MaterialTheme.colorScheme.onSurface,
- pressedOverlayAlpha = 0.15f,
- focusOutlineColor = MaterialTheme.colorScheme.secondary,
- focusOutlineStrokeWidth = 3.dp,
- focusOutlinePadding = 2.dp,
- surfaceCornerRadius = 28.dp,
- focusOutlineCornerRadius = 33.dp,
- ),
- ) {
- content()
- }
- } else {
- Surface(
- shape = shape,
- color = color.copy(0.38f),
- modifier = modifier.semantics { role = Role.Button }.width(width).height(height),
- ) {
- content()
- }
+ if (iconSource.imageVector != null) {
+ Icon(
+ tint = contentColor,
+ imageVector = iconSource.imageVector,
+ contentDescription = contentDescription,
+ modifier = Modifier.size(20.dp).wrapContentSize(Alignment.Center),
+ )
+ }
+
+ if (iconSource.imageVector != null && text != null)
+ Spacer(modifier = Modifier.width(8.dp))
+
+ if (text != null) {
+ Text(
+ text,
+ color = contentColor,
+ fontSize = 14.sp,
+ style = MaterialTheme.typography.labelLarge,
+ modifier = Modifier.wrapContentSize(Alignment.Center),
+ overflow = TextOverflow.Ellipsis,
+ )
}
}
+private fun Color.getDimmedColorIfDisabled(enabled: Boolean): Color =
+ if (enabled) this else copy(alpha = 0.38f)
+
@Composable
private fun surfaceColorAtElevation(color: Color, elevation: Dp): Color {
return MaterialTheme.colorScheme.applyTonalElevation(color, elevation)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index 2191f379e812..f1f299aac2b4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -45,16 +45,15 @@ import android.media.session.MediaController
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.net.Uri
-import android.os.Parcelable
import android.os.Process
import android.os.UserHandle
-import android.provider.Settings
import android.service.notification.StatusBarNotification
import android.support.v4.media.MediaMetadataCompat
import android.text.TextUtils
import android.util.Log
import android.util.Pair as APair
import androidx.media.utils.MediaConstants
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.app.tracing.traceSection
import com.android.internal.annotations.Keep
import com.android.internal.logging.InstanceId
@@ -86,11 +85,9 @@ import com.android.systemui.media.controls.util.MediaDataUtils
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.media.controls.util.SmallHash
-import com.android.systemui.plugins.BcSmartspaceDataPlugin
import com.android.systemui.res.R
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.notification.row.HybridGroupManager
-import com.android.systemui.tuner.TunerService
import com.android.systemui.util.Assert
import com.android.systemui.util.Utils
import com.android.systemui.util.concurrency.DelayableExecutor
@@ -103,7 +100,6 @@ import java.util.concurrent.Executor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
import kotlinx.coroutines.withContext
// URI fields to try loading album art from
@@ -152,22 +148,6 @@ internal val EMPTY_SMARTSPACE_MEDIA_DATA =
expiryTimeMs = 0,
)
-const val MEDIA_TITLE_ERROR_MESSAGE = "Invalid media data: title is null or blank."
-
-/**
- * Allow recommendations from smartspace to show in media controls. Requires
- * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0
- */
-private fun allowMediaRecommendations(context: Context): Boolean {
- val flag =
- Settings.Secure.getInt(
- context.contentResolver,
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
- 1,
- )
- return Utils.useQsMediaPlayer(context) && flag > 0
-}
-
/** A class that facilitates management and loading of Media Data, ready for binding. */
@SysUISingleton
class LegacyMediaDataManagerImpl(
@@ -191,14 +171,13 @@ class LegacyMediaDataManagerImpl(
private var useMediaResumption: Boolean,
private val useQsMediaPlayer: Boolean,
private val systemClock: SystemClock,
- private val tunerService: TunerService,
private val mediaFlags: MediaFlags,
private val logger: MediaUiEventLogger,
private val smartspaceManager: SmartspaceManager?,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val mediaDataLoader: dagger.Lazy<MediaDataLoader>,
private val mediaLogger: MediaLogger,
-) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener, MediaDataManager {
+) : Dumpable, MediaDataManager {
companion object {
// UI surface label for subscribing Smartspace updates.
@@ -238,7 +217,6 @@ class LegacyMediaDataManagerImpl(
// There should ONLY be at most one Smartspace media recommendation.
var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
@Keep private var smartspaceSession: SmartspaceSession? = null
- private var allowMediaRecommendations = allowMediaRecommendations(context)
private val artworkWidth =
context.resources.getDimensionPixelSize(
@@ -276,7 +254,6 @@ class LegacyMediaDataManagerImpl(
mediaDataFilter: LegacyMediaDataFilterImpl,
smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
clock: SystemClock,
- tunerService: TunerService,
mediaFlags: MediaFlags,
logger: MediaUiEventLogger,
smartspaceManager: SmartspaceManager?,
@@ -306,7 +283,6 @@ class LegacyMediaDataManagerImpl(
Utils.useMediaResumption(context),
Utils.useQsMediaPlayer(context),
clock,
- tunerService,
mediaFlags,
logger,
smartspaceManager,
@@ -372,7 +348,7 @@ class LegacyMediaDataManagerImpl(
context.registerReceiver(appChangeReceiver, uninstallFilter)
// Register for Smartspace data updates.
- smartspaceMediaDataProvider.registerListener(this)
+ // TODO(b/382680767): remove
smartspaceSession =
smartspaceManager?.createSmartspaceSession(
SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()
@@ -391,24 +367,9 @@ class LegacyMediaDataManagerImpl(
)
}
smartspaceSession?.let { it.requestSmartspaceUpdate() }
- tunerService.addTunable(
- object : TunerService.Tunable {
- override fun onTuningChanged(key: String?, newValue: String?) {
- allowMediaRecommendations = allowMediaRecommendations(context)
- if (!allowMediaRecommendations) {
- dismissSmartspaceRecommendation(
- key = smartspaceMediaData.targetId,
- delay = 0L,
- )
- }
- }
- },
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
- )
}
override fun destroy() {
- smartspaceMediaDataProvider.unregisterListener(this)
smartspaceSession?.close()
smartspaceSession = null
context.unregisterReceiver(appChangeReceiver)
@@ -1328,61 +1289,6 @@ class LegacyMediaDataManagerImpl(
}
}
- override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
- if (!allowMediaRecommendations) {
- if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.")
- return
- }
-
- val mediaTargets = targets.filterIsInstance<SmartspaceTarget>()
- when (mediaTargets.size) {
- 0 -> {
- if (!smartspaceMediaData.isActive) {
- return
- }
- if (DEBUG) {
- Log.d(TAG, "Set Smartspace media to be inactive for the data update")
- }
- if (mediaFlags.isPersistentSsCardEnabled()) {
- // Smartspace uses this signal to hide the card (e.g. when it expires or user
- // disconnects headphones), so treat as setting inactive when flag is on
- smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
- notifySmartspaceMediaDataLoaded(
- smartspaceMediaData.targetId,
- smartspaceMediaData,
- )
- } else {
- smartspaceMediaData =
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = smartspaceMediaData.targetId,
- instanceId = smartspaceMediaData.instanceId,
- )
- notifySmartspaceMediaDataRemoved(
- smartspaceMediaData.targetId,
- immediately = false,
- )
- }
- }
- 1 -> {
- val newMediaTarget = mediaTargets.get(0)
- if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) {
- // The same Smartspace updates can be received. Skip the duplicate updates.
- return
- }
- if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
- smartspaceMediaData = toSmartspaceMediaData(newMediaTarget)
- notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
- }
- else -> {
- // There should NOT be more than 1 Smartspace media update. When it happens, it
- // indicates a bad state or an error. Reset the status accordingly.
- Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
- notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
- smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
- }
- }
- }
-
override fun onNotificationRemoved(key: String) {
Assert.isMainThread()
val removed = mediaEntries.remove(key) ?: return
@@ -1641,7 +1547,6 @@ class LegacyMediaDataManagerImpl(
println("externalListeners: ${mediaDataFilter.listeners}")
println("mediaEntries: $mediaEntries")
println("useMediaResumption: $useMediaResumption")
- println("allowMediaRecommendations: $allowMediaRecommendations")
}
mediaDeviceManager.dump(pw)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index 3821f3d8c111..a524db4437a5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -45,16 +45,15 @@ import android.media.session.MediaController
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.net.Uri
-import android.os.Parcelable
import android.os.Process
import android.os.UserHandle
-import android.provider.Settings
import android.service.notification.StatusBarNotification
import android.support.v4.media.MediaMetadataCompat
import android.text.TextUtils
import android.util.Log
import android.util.Pair as APair
import androidx.media.utils.MediaConstants
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.app.tracing.traceSection
import com.android.internal.annotations.Keep
import com.android.internal.logging.InstanceId
@@ -87,8 +86,6 @@ import com.android.systemui.media.controls.util.MediaDataUtils
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.media.controls.util.SmallHash
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.BcSmartspaceDataPlugin
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
@@ -97,8 +94,6 @@ import com.android.systemui.util.Assert
import com.android.systemui.util.Utils
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.concurrency.ThreadFactory
-import com.android.systemui.util.settings.SecureSettings
-import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
import com.android.systemui.util.time.SystemClock
import java.io.IOException
import java.io.PrintWriter
@@ -106,12 +101,6 @@ import java.util.concurrent.Executor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
-import com.android.app.tracing.coroutines.launchTraced as launch
import kotlinx.coroutines.withContext
// URI fields to try loading album art from
@@ -139,12 +128,10 @@ class MediaDataProcessor(
private val mediaControllerFactory: MediaControllerFactory,
private val broadcastDispatcher: BroadcastDispatcher,
private val dumpManager: DumpManager,
- private val activityStarter: ActivityStarter,
private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
private var useMediaResumption: Boolean,
private val useQsMediaPlayer: Boolean,
private val systemClock: SystemClock,
- private val secureSettings: SecureSettings,
private val mediaFlags: MediaFlags,
private val logger: MediaUiEventLogger,
private val smartspaceManager: SmartspaceManager?,
@@ -152,7 +139,7 @@ class MediaDataProcessor(
private val mediaDataRepository: MediaDataRepository,
private val mediaDataLoader: dagger.Lazy<MediaDataLoader>,
private val mediaLogger: MediaLogger,
-) : CoreStartable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
+) : CoreStartable {
companion object {
/**
@@ -191,7 +178,6 @@ class MediaDataProcessor(
// There should ONLY be at most one Smartspace media recommendation.
@Keep private var smartspaceSession: SmartspaceSession? = null
- private var allowMediaRecommendations = false
private val artworkWidth =
context.resources.getDimensionPixelSize(
@@ -221,10 +207,8 @@ class MediaDataProcessor(
mediaControllerFactory: MediaControllerFactory,
dumpManager: DumpManager,
broadcastDispatcher: BroadcastDispatcher,
- activityStarter: ActivityStarter,
smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
clock: SystemClock,
- secureSettings: SecureSettings,
mediaFlags: MediaFlags,
logger: MediaUiEventLogger,
smartspaceManager: SmartspaceManager?,
@@ -245,12 +229,10 @@ class MediaDataProcessor(
mediaControllerFactory,
broadcastDispatcher,
dumpManager,
- activityStarter,
smartspaceMediaDataProvider,
Utils.useMediaResumption(context),
Utils.useQsMediaPlayer(context),
clock,
- secureSettings,
mediaFlags,
logger,
smartspaceManager,
@@ -296,7 +278,7 @@ class MediaDataProcessor(
context.registerReceiver(appChangeReceiver, uninstallFilter)
// Register for Smartspace data updates.
- smartspaceMediaDataProvider.registerListener(this)
+ // TODO(b/382680767): remove
smartspaceSession =
smartspaceManager?.createSmartspaceSession(
SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()
@@ -314,13 +296,9 @@ class MediaDataProcessor(
}
}
smartspaceSession?.requestSmartspaceUpdate()
-
- // Track media controls recommendation setting.
- applicationScope.launch { trackMediaControlsRecommendationSetting() }
}
fun destroy() {
- smartspaceMediaDataProvider.unregisterListener(this)
smartspaceSession?.close()
smartspaceSession = null
context.unregisterReceiver(appChangeReceiver)
@@ -357,43 +335,6 @@ class MediaDataProcessor(
}
}
- /**
- * Allow recommendations from smartspace to show in media controls. Requires
- * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0
- */
- private suspend fun allowMediaRecommendations(): Boolean {
- return withContext(backgroundDispatcher) {
- val flag =
- secureSettings.getBoolForUser(
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
- true,
- UserHandle.USER_CURRENT,
- )
-
- useQsMediaPlayer && flag
- }
- }
-
- private suspend fun trackMediaControlsRecommendationSetting() {
- secureSettings
- .observerFlow(UserHandle.USER_ALL, Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)
- // perform a query at the beginning.
- .onStart { emit(Unit) }
- .map { allowMediaRecommendations() }
- .distinctUntilChanged()
- .flowOn(backgroundDispatcher)
- // only track the most recent emission
- .collectLatest {
- allowMediaRecommendations = it
- if (!allowMediaRecommendations) {
- dismissSmartspaceRecommendation(
- key = mediaDataRepository.smartspaceMediaData.value.targetId,
- delay = 0L,
- )
- }
- }
- }
-
private fun removeAllForPackage(packageName: String) {
Assert.isMainThread()
val toRemove =
@@ -1277,62 +1218,6 @@ class MediaDataProcessor(
}
}
- override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
- if (!allowMediaRecommendations) {
- if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.")
- return
- }
-
- val mediaTargets = targets.filterIsInstance<SmartspaceTarget>()
- val smartspaceMediaData = mediaDataRepository.smartspaceMediaData.value
- when (mediaTargets.size) {
- 0 -> {
- if (!smartspaceMediaData.isActive) {
- return
- }
- if (DEBUG) {
- Log.d(TAG, "Set Smartspace media to be inactive for the data update")
- }
- if (mediaFlags.isPersistentSsCardEnabled()) {
- // Smartspace uses this signal to hide the card (e.g. when it expires or user
- // disconnects headphones), so treat as setting inactive when flag is on
- val recommendation = smartspaceMediaData.copy(isActive = false)
- mediaDataRepository.setRecommendation(recommendation)
- notifySmartspaceMediaDataLoaded(recommendation.targetId, recommendation)
- } else {
- notifySmartspaceMediaDataRemoved(
- smartspaceMediaData.targetId,
- immediately = false,
- )
- mediaDataRepository.setRecommendation(
- SmartspaceMediaData(
- targetId = smartspaceMediaData.targetId,
- instanceId = smartspaceMediaData.instanceId,
- )
- )
- }
- }
- 1 -> {
- val newMediaTarget = mediaTargets.get(0)
- if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) {
- // The same Smartspace updates can be received. Skip the duplicate updates.
- return
- }
- if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
- val recommendation = toSmartspaceMediaData(newMediaTarget)
- mediaDataRepository.setRecommendation(recommendation)
- notifySmartspaceMediaDataLoaded(recommendation.targetId, recommendation)
- }
- else -> {
- // There should NOT be more than 1 Smartspace media update. When it happens, it
- // indicates a bad state or an error. Reset the status accordingly.
- Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
- notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
- mediaDataRepository.setRecommendation(SmartspaceMediaData())
- }
- }
- }
-
fun onNotificationRemoved(key: String) {
Assert.isMainThread()
val removed = mediaDataRepository.removeMediaEntry(key) ?: return
@@ -1621,7 +1506,6 @@ class MediaDataProcessor(
pw.apply {
println("internalListeners: $internalListeners")
println("useMediaResumption: $useMediaResumption")
- println("allowMediaRecommendations: $allowMediaRecommendations")
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
index 86e92941256b..975f8f45f9c4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
@@ -1042,6 +1042,18 @@ constructor(
return null
}
+ if (state.expansion == 1.0f) {
+ val height =
+ if (state.expandedMatchesParentHeight) {
+ heightInSceneContainerPx
+ } else {
+ context.resources.getDimensionPixelSize(
+ R.dimen.qs_media_session_height_expanded
+ )
+ }
+ setBackgroundHeights(height)
+ }
+
// Similar to obtainViewState: Let's create a new measurement
val result =
transitionLayout?.calculateViewState(
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
index f53b6cd29806..57d40638b8df 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
@@ -39,7 +39,6 @@ import androidx.annotation.DrawableRes
import androidx.annotation.WorkerThread
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
-import com.android.settingslib.Utils
import com.android.systemui.animation.ViewHierarchyAnimator
import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.SystemUIDialog
@@ -63,7 +62,7 @@ class PrivacyDialogV2(
private val list: List<PrivacyElement>,
private val manageApp: (String, Int, Intent) -> Unit,
private val closeApp: (String, Int) -> Unit,
- private val openPrivacyDashboard: () -> Unit
+ private val openPrivacyDashboard: () -> Unit,
) : SystemUIDialog(context, R.style.Theme_PrivacyDialog) {
private val dismissListeners = mutableListOf<WeakReference<OnDialogDismissed>>()
@@ -192,11 +191,9 @@ class PrivacyDialogV2(
return null
}
val closeAppButton =
- checkNotNull(window).layoutInflater.inflate(
- R.layout.privacy_dialog_card_button,
- expandedLayout,
- false
- ) as Button
+ checkNotNull(window)
+ .layoutInflater
+ .inflate(R.layout.privacy_dialog_card_button, expandedLayout, false) as Button
expandedLayout.addView(closeAppButton)
closeAppButton.id = R.id.privacy_dialog_close_app_button
closeAppButton.setText(R.string.privacy_dialog_close_app_button)
@@ -248,11 +245,9 @@ class PrivacyDialogV2(
private fun configureManageButton(element: PrivacyElement, expandedLayout: ViewGroup): View {
val manageButton =
- checkNotNull(window).layoutInflater.inflate(
- R.layout.privacy_dialog_card_button,
- expandedLayout,
- false
- ) as Button
+ checkNotNull(window)
+ .layoutInflater
+ .inflate(R.layout.privacy_dialog_card_button, expandedLayout, false) as Button
expandedLayout.addView(manageButton)
manageButton.id = R.id.privacy_dialog_manage_app_button
manageButton.setText(
@@ -294,7 +289,7 @@ class PrivacyDialogV2(
itemCard,
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
context.getString(R.string.privacy_dialog_expand_action),
- null
+ null,
)
val expandedLayout =
@@ -311,7 +306,7 @@ class PrivacyDialogV2(
it!!,
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
context.getString(R.string.privacy_dialog_expand_action),
- null
+ null,
)
} else {
expandedLayout.visibility = View.VISIBLE
@@ -320,12 +315,12 @@ class PrivacyDialogV2(
it!!,
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
context.getString(R.string.privacy_dialog_collapse_action),
- null
+ null,
)
}
ViewHierarchyAnimator.animateNextUpdate(
rootView = window!!.decorView,
- excludedViews = setOf(expandedLayout)
+ excludedViews = setOf(expandedLayout),
)
}
}
@@ -345,19 +340,13 @@ class PrivacyDialogV2(
@ColorInt
private fun getForegroundColor(active: Boolean) =
- Utils.getColorAttrDefaultColor(
- context,
- if (active) com.android.internal.R.color.materialColorOnPrimaryFixed
- else com.android.internal.R.color.materialColorOnSurface,
- )
+ if (active) context.getColor(com.android.internal.R.color.materialColorOnPrimaryFixed)
+ else context.getColor(com.android.internal.R.color.materialColorOnSurface)
@ColorInt
private fun getBackgroundColor(active: Boolean) =
- Utils.getColorAttrDefaultColor(
- context,
- if (active) com.android.internal.R.color.materialColorPrimaryFixed
- else com.android.internal.R.color.materialColorSurfaceContainerHigh,
- )
+ if (active) context.getColor(com.android.internal.R.color.materialColorPrimaryFixed)
+ else context.getColor(com.android.internal.R.color.materialColorSurfaceContainerHigh)
private fun getMutableDrawable(@DrawableRes resId: Int) = context.getDrawable(resId)!!.mutate()
@@ -379,7 +368,7 @@ class PrivacyDialogV2(
context.getString(
singleUsageResId,
element.applicationName,
- element.attributionLabel ?: element.proxyLabel
+ element.attributionLabel ?: element.proxyLabel,
)
} else {
val doubleUsageResId: Int =
@@ -389,7 +378,7 @@ class PrivacyDialogV2(
doubleUsageResId,
element.applicationName,
element.attributionLabel,
- element.proxyLabel
+ element.proxyLabel,
)
}
@@ -429,7 +418,7 @@ class PrivacyDialogV2(
return groupInfo.loadSafeLabel(
this,
0f,
- TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
+ TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM,
)
}
@@ -472,7 +461,7 @@ class PrivacyDialogV2(
icon: Drawable,
iconSize: Int,
background: Drawable,
- backgroundSize: Int
+ backgroundSize: Int,
): Drawable {
val layered = LayerDrawable(arrayOf(background, icon))
layered.setLayerSize(0, backgroundSize, backgroundSize)
@@ -497,7 +486,7 @@ class PrivacyDialogV2(
val isPhoneCall: Boolean,
val isService: Boolean,
val permGroupName: String,
- val navigationIntent: Intent
+ val navigationIntent: Intent,
) {
private val builder = StringBuilder("PrivacyElement(")
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
index 05ee35b36c7f..8f5fccd7e324 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
@@ -75,8 +75,7 @@ public class TunerServiceImpl extends TunerService {
private static final String[] RESET_EXCEPTION_LIST = new String[] {
QSHost.TILES_SETTING,
Settings.Secure.DOZE_ALWAYS_ON,
- Settings.Secure.MEDIA_CONTROLS_RESUME,
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION
+ Settings.Secure.MEDIA_CONTROLS_RESUME
};
private final Observer mObserver = new Observer();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index 3ddd4b58211d..2815b97691ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -41,13 +41,11 @@ import android.os.Bundle
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
-import android.provider.Settings
import android.service.notification.StatusBarNotification
import android.testing.TestableLooper.RunWithLooper
import androidx.media.utils.MediaConstants
import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito
-import com.android.internal.logging.InstanceId
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.Flags
import com.android.systemui.InstanceIdSequenceFake
@@ -65,10 +63,7 @@ import com.android.systemui.media.controls.domain.resume.MediaResumeListener
import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
import com.android.systemui.media.controls.shared.mediaLogger
import com.android.systemui.media.controls.shared.mockMediaLogger
-import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
-import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.media.controls.util.fakeMediaControllerFactory
@@ -76,7 +71,6 @@ import com.android.systemui.media.controls.util.mediaFlags
import com.android.systemui.res.R
import com.android.systemui.statusbar.SbnBuilder
import com.android.systemui.testKosmos
-import com.android.systemui.tuner.TunerService
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -95,12 +89,10 @@ import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Captor
import org.mockito.Mock
-import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoSession
import org.mockito.junit.MockitoJUnit
@@ -126,10 +118,6 @@ private const val SESSION_EMPTY_TITLE = ""
private const val USER_ID = 0
private val DISMISS_INTENT = Intent().apply { action = "dismiss" }
-private fun <T> anyObject(): T {
- return Mockito.anyObject<T>()
-}
-
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWithLooper(setAsMainLooper = true)
@@ -168,8 +156,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
lateinit var remoteCastNotification: StatusBarNotification
@Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
private val clock = FakeSystemClock()
- @Mock private lateinit var tunerService: TunerService
- @Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable>
@Captor lateinit var stateCallbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit>
@Captor lateinit var sessionCallbackCaptor: ArgumentCaptor<(String) -> Unit>
@Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig>
@@ -197,13 +183,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
private val mediaControllerFactory = kosmos.fakeMediaControllerFactory
private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
- private val originalSmartspaceSetting =
- Settings.Secure.getInt(
- context.contentResolver,
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
- 1,
- )
-
private lateinit var staticMockSession: MockitoSession
@Before
@@ -219,11 +198,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
backgroundExecutor = FakeExecutor(clock)
uiExecutor = FakeExecutor(clock)
smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
- Settings.Secure.putInt(
- context.contentResolver,
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
- 1,
- )
mediaDataManager =
LegacyMediaDataManagerImpl(
context = context,
@@ -246,7 +220,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
useMediaResumption = true,
useQsMediaPlayer = true,
systemClock = clock,
- tunerService = tunerService,
mediaFlags = kosmos.mediaFlags,
logger = logger,
smartspaceManager = smartspaceManager,
@@ -254,8 +227,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
mediaDataLoader = { kosmos.mediaDataLoader },
mediaLogger = kosmos.mediaLogger,
)
- verify(tunerService)
- .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
verify(mediaTimeoutListener).stateCallback = capture(stateCallbackCaptor)
verify(mediaTimeoutListener).sessionCallback = capture(sessionCallbackCaptor)
session = MediaSession(context, "MediaDataManagerTestSession")
@@ -332,11 +303,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
staticMockSession.finishMocking()
session.release()
mediaDataManager.destroy()
- Settings.Secure.putInt(
- context.contentResolver,
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
- originalSmartspaceSetting,
- )
}
@Test
@@ -1236,272 +1202,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
}
@Test
- fun testOnSmartspaceMediaDataLoaded_hasNewValidMediaTarget_callsListener() {
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- verify(logger).getNewInstanceId()
- val instanceId = instanceIdSequence.lastInstanceId
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(
- eq(KEY_MEDIA_SMARTSPACE),
- eq(
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = true,
- packageName = PACKAGE_NAME,
- cardAction = mediaSmartspaceBaseAction,
- recommendations = validRecommendationList,
- dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
- instanceId = InstanceId.fakeInstanceId(instanceId),
- expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
- )
- ),
- eq(false),
- )
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_hasNewInvalidMediaTarget_callsListener() {
- whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- verify(logger).getNewInstanceId()
- val instanceId = instanceIdSequence.lastInstanceId
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(
- eq(KEY_MEDIA_SMARTSPACE),
- eq(
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = true,
- dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
- instanceId = InstanceId.fakeInstanceId(instanceId),
- expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
- )
- ),
- eq(false),
- )
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_hasNullIntent_callsListener() {
- val recommendationExtras =
- Bundle().apply {
- putString("package_name", PACKAGE_NAME)
- putParcelable("dismiss_intent", null)
- }
- whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras)
- whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction)
- whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
-
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- verify(logger).getNewInstanceId()
- val instanceId = instanceIdSequence.lastInstanceId
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(
- eq(KEY_MEDIA_SMARTSPACE),
- eq(
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = true,
- dismissIntent = null,
- headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
- instanceId = InstanceId.fakeInstanceId(instanceId),
- expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
- )
- ),
- eq(false),
- )
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListener() {
- smartspaceMediaDataProvider.onTargetsAvailable(listOf())
- verify(logger, never()).getNewInstanceId()
- verify(listener, never())
- .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListener() {
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- verify(logger).getNewInstanceId()
-
- smartspaceMediaDataProvider.onTargetsAvailable(listOf())
- uiExecutor.advanceClockToLast()
- uiExecutor.runAllReady()
-
- verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false))
- verifyNoMoreInteractions(logger)
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_persistentEnabled_headphoneTrigger_isActive() {
- fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true)
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- val instanceId = instanceIdSequence.lastInstanceId
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(
- eq(KEY_MEDIA_SMARTSPACE),
- eq(
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = true,
- packageName = PACKAGE_NAME,
- cardAction = mediaSmartspaceBaseAction,
- recommendations = validRecommendationList,
- dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
- instanceId = InstanceId.fakeInstanceId(instanceId),
- expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
- )
- ),
- eq(false),
- )
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_persistentEnabled_periodicTrigger_notActive() {
- fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true)
- val extras =
- Bundle().apply {
- putString("package_name", PACKAGE_NAME)
- putParcelable("dismiss_intent", DISMISS_INTENT)
- putString(EXTRA_KEY_TRIGGER_SOURCE, EXTRA_VALUE_TRIGGER_PERIODIC)
- }
- whenever(mediaSmartspaceBaseAction.extras).thenReturn(extras)
-
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- val instanceId = instanceIdSequence.lastInstanceId
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(
- eq(KEY_MEDIA_SMARTSPACE),
- eq(
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = false,
- packageName = PACKAGE_NAME,
- cardAction = mediaSmartspaceBaseAction,
- recommendations = validRecommendationList,
- dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
- instanceId = InstanceId.fakeInstanceId(instanceId),
- expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
- )
- ),
- eq(false),
- )
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_persistentEnabled_noTargets_inactive() {
- fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true)
-
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- val instanceId = instanceIdSequence.lastInstanceId
-
- smartspaceMediaDataProvider.onTargetsAvailable(listOf())
- uiExecutor.advanceClockToLast()
- uiExecutor.runAllReady()
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(
- eq(KEY_MEDIA_SMARTSPACE),
- eq(
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = false,
- packageName = PACKAGE_NAME,
- cardAction = mediaSmartspaceBaseAction,
- recommendations = validRecommendationList,
- dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
- instanceId = InstanceId.fakeInstanceId(instanceId),
- expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
- )
- ),
- eq(false),
- )
- verify(listener, never()).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false))
- }
-
- @Test
- fun testSetRecommendationInactive_notifiesListeners() {
- fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true)
-
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- val instanceId = instanceIdSequence.lastInstanceId
-
- mediaDataManager.setRecommendationInactive(KEY_MEDIA_SMARTSPACE)
- uiExecutor.advanceClockToLast()
- uiExecutor.runAllReady()
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(
- eq(KEY_MEDIA_SMARTSPACE),
- eq(
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = false,
- packageName = PACKAGE_NAME,
- cardAction = mediaSmartspaceBaseAction,
- recommendations = validRecommendationList,
- dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
- instanceId = InstanceId.fakeInstanceId(instanceId),
- expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
- )
- ),
- eq(false),
- )
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
- // WHEN media recommendation setting is off
- Settings.Secure.putInt(
- context.contentResolver,
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
- 0,
- )
- tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
-
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
-
- // THEN smartspace signal is ignored
- verify(listener, never())
- .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
- }
-
- @Test
- fun testMediaRecommendationDisabled_removesSmartspaceData() {
- // GIVEN a media recommendation card is present
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- verify(listener)
- .onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(), anyBoolean())
-
- // WHEN the media recommendation setting is turned off
- Settings.Secure.putInt(
- context.contentResolver,
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
- 0,
- )
- tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
-
- // THEN listeners are notified
- uiExecutor.advanceClockToLast()
- foregroundExecutor.advanceClockToLast()
- uiExecutor.runAllReady()
- foregroundExecutor.runAllReady()
- verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(true))
- }
-
- @Test
fun testOnMediaDataChanged_updatesLastActiveTime() {
val currentTime = clock.elapsedRealtime()
addNotificationAndLoad()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index e5483c0980c0..b9ebce816c5c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -42,13 +42,11 @@ import android.os.UserHandle
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
-import android.provider.Settings
import android.service.notification.StatusBarNotification
import android.testing.TestableLooper.RunWithLooper
import androidx.media.utils.MediaConstants
import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito
-import com.android.internal.logging.InstanceId
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.Flags
import com.android.systemui.InstanceIdSequenceFake
@@ -71,21 +69,16 @@ import com.android.systemui.media.controls.domain.resume.MediaResumeListener
import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
import com.android.systemui.media.controls.shared.mediaLogger
import com.android.systemui.media.controls.shared.mockMediaLogger
-import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
-import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.media.controls.util.fakeMediaControllerFactory
import com.android.systemui.media.controls.util.mediaFlags
-import com.android.systemui.plugins.activityStarter
import com.android.systemui.res.R
import com.android.systemui.statusbar.SbnBuilder
import com.android.systemui.statusbar.notificationLockscreenUserManager
import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.settings.fakeSettings
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -102,11 +95,9 @@ import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Captor
import org.mockito.Mock
-import org.mockito.Mockito
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoSession
import org.mockito.junit.MockitoJUnit
import org.mockito.kotlin.any
@@ -133,10 +124,6 @@ private const val SESSION_EMPTY_TITLE = ""
private const val USER_ID = 0
private val DISMISS_INTENT = Intent().apply { action = "dismiss" }
-private fun <T> anyObject(): T {
- return Mockito.anyObject<T>()
-}
-
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWithLooper(setAsMainLooper = true)
@@ -146,7 +133,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos = testKosmos().apply { mediaLogger = mockMediaLogger }
private val testDispatcher = kosmos.testDispatcher
private val testScope = kosmos.testScope
- private val settings = kosmos.fakeSettings
@JvmField @Rule val mockito = MockitoJUnit.rule()
@Mock lateinit var controller: MediaController
@@ -201,7 +187,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
}
private val fakeFeatureFlags = kosmos.fakeFeatureFlagsClassic
- private val activityStarter = kosmos.activityStarter
private val mediaControllerFactory = kosmos.fakeMediaControllerFactory
private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager
private val mediaFilterRepository = kosmos.mediaFilterRepository
@@ -209,13 +194,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
- private val originalSmartspaceSetting =
- Settings.Secure.getInt(
- context.contentResolver,
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
- 1,
- )
-
private lateinit var staticMockSession: MockitoSession
@Before
@@ -231,11 +209,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
backgroundExecutor = FakeExecutor(clock)
uiExecutor = FakeExecutor(clock)
smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
- Settings.Secure.putInt(
- context.contentResolver,
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
- 1,
- )
mediaDataProcessor =
MediaDataProcessor(
context = context,
@@ -248,12 +221,10 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
mediaControllerFactory = mediaControllerFactory,
broadcastDispatcher = broadcastDispatcher,
dumpManager = dumpManager,
- activityStarter = activityStarter,
smartspaceMediaDataProvider = smartspaceMediaDataProvider,
useMediaResumption = true,
useQsMediaPlayer = true,
systemClock = clock,
- secureSettings = settings,
mediaFlags = kosmos.mediaFlags,
logger = logger,
smartspaceManager = smartspaceManager,
@@ -355,11 +326,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
staticMockSession.finishMocking()
session.release()
mediaDataProcessor.destroy()
- Settings.Secure.putInt(
- context.contentResolver,
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
- originalSmartspaceSetting,
- )
}
@Test
@@ -1255,264 +1221,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- fun testOnSmartspaceMediaDataLoaded_hasNewValidMediaTarget_callsListener() {
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- verify(logger).getNewInstanceId()
- val instanceId = instanceIdSequence.lastInstanceId
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(
- eq(KEY_MEDIA_SMARTSPACE),
- eq(
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = true,
- packageName = PACKAGE_NAME,
- cardAction = mediaSmartspaceBaseAction,
- recommendations = validRecommendationList,
- dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
- instanceId = InstanceId.fakeInstanceId(instanceId),
- expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
- )
- ),
- eq(false),
- )
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_hasNewInvalidMediaTarget_callsListener() {
- whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- verify(logger).getNewInstanceId()
- val instanceId = instanceIdSequence.lastInstanceId
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(
- eq(KEY_MEDIA_SMARTSPACE),
- eq(
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = true,
- dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
- instanceId = InstanceId.fakeInstanceId(instanceId),
- expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
- )
- ),
- eq(false),
- )
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_hasNullIntent_callsListener() {
- val recommendationExtras =
- Bundle().apply {
- putString("package_name", PACKAGE_NAME)
- putParcelable("dismiss_intent", null)
- }
- whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras)
- whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction)
- whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
-
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- verify(logger).getNewInstanceId()
- val instanceId = instanceIdSequence.lastInstanceId
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(
- eq(KEY_MEDIA_SMARTSPACE),
- eq(
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = true,
- dismissIntent = null,
- headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
- instanceId = InstanceId.fakeInstanceId(instanceId),
- expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
- )
- ),
- eq(false),
- )
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListener() {
- smartspaceMediaDataProvider.onTargetsAvailable(listOf())
- verify(logger, never()).getNewInstanceId()
- verify(listener, never())
- .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListener() {
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- verify(logger).getNewInstanceId()
-
- smartspaceMediaDataProvider.onTargetsAvailable(listOf())
- uiExecutor.advanceClockToLast()
- uiExecutor.runAllReady()
-
- verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false))
- verifyNoMoreInteractions(logger)
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_persistentEnabled_headphoneTrigger_isActive() {
- fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true)
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- val instanceId = instanceIdSequence.lastInstanceId
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(
- eq(KEY_MEDIA_SMARTSPACE),
- eq(
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = true,
- packageName = PACKAGE_NAME,
- cardAction = mediaSmartspaceBaseAction,
- recommendations = validRecommendationList,
- dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
- instanceId = InstanceId.fakeInstanceId(instanceId),
- expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
- )
- ),
- eq(false),
- )
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_persistentEnabled_periodicTrigger_notActive() {
- fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true)
- val extras =
- Bundle().apply {
- putString("package_name", PACKAGE_NAME)
- putParcelable("dismiss_intent", DISMISS_INTENT)
- putString(EXTRA_KEY_TRIGGER_SOURCE, EXTRA_VALUE_TRIGGER_PERIODIC)
- }
- whenever(mediaSmartspaceBaseAction.extras).thenReturn(extras)
-
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- val instanceId = instanceIdSequence.lastInstanceId
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(
- eq(KEY_MEDIA_SMARTSPACE),
- eq(
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = false,
- packageName = PACKAGE_NAME,
- cardAction = mediaSmartspaceBaseAction,
- recommendations = validRecommendationList,
- dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
- instanceId = InstanceId.fakeInstanceId(instanceId),
- expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
- )
- ),
- eq(false),
- )
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_persistentEnabled_noTargets_inactive() {
- fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true)
-
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- val instanceId = instanceIdSequence.lastInstanceId
-
- smartspaceMediaDataProvider.onTargetsAvailable(listOf())
- uiExecutor.advanceClockToLast()
- uiExecutor.runAllReady()
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(
- eq(KEY_MEDIA_SMARTSPACE),
- eq(
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = false,
- packageName = PACKAGE_NAME,
- cardAction = mediaSmartspaceBaseAction,
- recommendations = validRecommendationList,
- dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
- instanceId = InstanceId.fakeInstanceId(instanceId),
- expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
- )
- ),
- eq(false),
- )
- verify(listener, never()).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false))
- }
-
- @Test
- fun testSetRecommendationInactive_notifiesListeners() {
- fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true)
-
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- val instanceId = instanceIdSequence.lastInstanceId
-
- mediaDataProcessor.setRecommendationInactive(KEY_MEDIA_SMARTSPACE)
- uiExecutor.advanceClockToLast()
- uiExecutor.runAllReady()
-
- verify(listener)
- .onSmartspaceMediaDataLoaded(
- eq(KEY_MEDIA_SMARTSPACE),
- eq(
- SmartspaceMediaData(
- targetId = KEY_MEDIA_SMARTSPACE,
- isActive = false,
- packageName = PACKAGE_NAME,
- cardAction = mediaSmartspaceBaseAction,
- recommendations = validRecommendationList,
- dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
- instanceId = InstanceId.fakeInstanceId(instanceId),
- expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
- )
- ),
- eq(false),
- )
- }
-
- @Test
- fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
- // WHEN media recommendation setting is off
- settings.putInt(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0)
- testScope.runCurrent()
-
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
-
- // THEN smartspace signal is ignored
- verify(listener, never())
- .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
- }
-
- @Test
- fun testMediaRecommendationDisabled_removesSmartspaceData() {
- // GIVEN a media recommendation card is present
- smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
- verify(listener)
- .onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(), anyBoolean())
-
- // WHEN the media recommendation setting is turned off
- settings.putInt(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0)
- testScope.runCurrent()
-
- // THEN listeners are notified
- uiExecutor.advanceClockToLast()
- foregroundExecutor.advanceClockToLast()
- uiExecutor.runAllReady()
- foregroundExecutor.runAllReady()
- verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(true))
- }
-
- @Test
fun testOnMediaDataChanged_updatesLastActiveTime() {
val currentTime = clock.elapsedRealtime()
addNotificationAndLoad()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
index 174e6532abcf..fcaad6bb28ea 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
@@ -31,9 +31,7 @@ import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvi
import com.android.systemui.media.controls.util.fakeMediaControllerFactory
import com.android.systemui.media.controls.util.mediaFlags
import com.android.systemui.media.controls.util.mediaUiEventLogger
-import com.android.systemui.plugins.activityStarter
import com.android.systemui.util.Utils
-import com.android.systemui.util.settings.fakeSettings
import com.android.systemui.util.time.systemClock
val Kosmos.mediaDataProcessor by
@@ -49,12 +47,10 @@ val Kosmos.mediaDataProcessor by
mediaControllerFactory = fakeMediaControllerFactory,
broadcastDispatcher = broadcastDispatcher,
dumpManager = dumpManager,
- activityStarter = activityStarter,
smartspaceMediaDataProvider = SmartspaceMediaDataProvider(),
useMediaResumption = Utils.useMediaResumption(applicationContext),
useQsMediaPlayer = Utils.useQsMediaPlayer(applicationContext),
systemClock = systemClock,
- secureSettings = fakeSettings,
mediaFlags = mediaFlags,
logger = mediaUiEventLogger,
smartspaceManager = SmartspaceManager(applicationContext),
diff --git a/packages/SystemUI/utils/Android.bp b/packages/SystemUI/utils/Android.bp
index 1ef381617c20..1efb11b436ff 100644
--- a/packages/SystemUI/utils/Android.bp
+++ b/packages/SystemUI/utils/Android.bp
@@ -29,4 +29,5 @@ java_library {
"kotlin-stdlib",
"kotlinx_coroutines",
],
+ kotlincflags: ["-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"],
}
diff --git a/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/LatestConflated.kt b/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/LatestConflated.kt
index 5f8c66078483..4cfb7d5f6157 100644
--- a/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/LatestConflated.kt
+++ b/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/LatestConflated.kt
@@ -14,12 +14,11 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalTypeInference::class)
+@file:OptIn(ExperimentalTypeInference::class)
package com.android.systemui.utils.coroutines.flow
import kotlin.experimental.ExperimentalTypeInference
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.conflate
diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
index f8315fe1e55f..383e75bb5122 100644
--- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
@@ -115,6 +115,7 @@ android.util.UtilConfig
android.util.Xml
android.util.proto.EncodedBuffer
+android.util.proto.ProtoFieldFilter
android.util.proto.ProtoInputStream
android.util.proto.ProtoOutputStream
android.util.proto.ProtoParseException
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index 280494544e3b..3508f2ffc4c4 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -106,7 +106,7 @@ class CompanionDeviceShellCommand extends ShellCommand {
boolean selfManaged = getNextBooleanArg();
final MacAddress macAddress = MacAddress.fromString(address);
mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress,
- deviceProfile, deviceProfile, /* associatedDevice */ null, false,
+ deviceProfile, deviceProfile, /* associatedDevice */ null, selfManaged,
/* callback */ null, /* resultReceiver */ null, /* deviceIcon */ null);
}
break;
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 0b335d318d64..4dd8b141386f 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -820,13 +820,12 @@ public class VirtualDeviceManagerService extends SystemService {
@Override
public void onAuthenticationPrompt(int uid) {
- synchronized (mVirtualDeviceManagerLock) {
- for (int i = 0; i < mVirtualDevices.size(); i++) {
- VirtualDeviceImpl device = mVirtualDevices.valueAt(i);
- device.showToastWhereUidIsRunning(uid,
- R.string.app_streaming_blocked_message_for_fingerprint_dialog,
- Toast.LENGTH_LONG, Looper.getMainLooper());
- }
+ ArrayList<VirtualDeviceImpl> virtualDevicesSnapshot = getVirtualDevicesSnapshot();
+ for (int i = 0; i < virtualDevicesSnapshot.size(); i++) {
+ VirtualDeviceImpl device = virtualDevicesSnapshot.get(i);
+ device.showToastWhereUidIsRunning(uid,
+ R.string.app_streaming_blocked_message_for_fingerprint_dialog,
+ Toast.LENGTH_LONG, Looper.getMainLooper());
}
}
diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java
index 1588e0421675..7a5b8660ef7c 100644
--- a/services/core/java/com/android/server/BootReceiver.java
+++ b/services/core/java/com/android/server/BootReceiver.java
@@ -40,7 +40,9 @@ import android.util.AtomicFile;
import android.util.EventLog;
import android.util.Slog;
import android.util.Xml;
+import android.util.proto.ProtoFieldFilter;
import android.util.proto.ProtoOutputStream;
+import android.util.proto.ProtoParseException;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
@@ -49,10 +51,13 @@ import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.am.DropboxRateLimiter;
+import com.android.server.os.TombstoneProtos.Tombstone;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
@@ -64,6 +69,7 @@ import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.HashMap;
import java.util.Iterator;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -392,6 +398,129 @@ public class BootReceiver extends BroadcastReceiver {
writeTimestamps(timestamps);
}
+ /**
+ * Processes a tombstone file and adds it to the DropBox after filtering and applying
+ * rate limiting.
+ * Filtering removes memory sections from the tombstone proto to reduce size while preserving
+ * critical information. The filtered tombstone is then added to DropBox in both proto
+ * and text formats, with the text format derived from the filtered proto.
+ * Rate limiting is applied as it is the case with other crash types.
+ *
+ * @param ctx Context
+ * @param tombstone path to the tombstone
+ * @param processName the name of the process corresponding to the tombstone
+ * @param tmpFileLock the lock for reading/writing tmp files
+ */
+ public static void filterAndAddTombstoneToDropBox(
+ Context ctx, File tombstone, String processName, ReentrantLock tmpFileLock) {
+ final DropBoxManager db = ctx.getSystemService(DropBoxManager.class);
+ if (db == null) {
+ Slog.e(TAG, "Can't log tombstone: DropBoxManager not available");
+ return;
+ }
+ File filteredProto = null;
+ // Check if we should rate limit and abort early if needed.
+ DropboxRateLimiter.RateLimitResult rateLimitResult =
+ sDropboxRateLimiter.shouldRateLimit(TAG_TOMBSTONE_PROTO_WITH_HEADERS, processName);
+ if (rateLimitResult.shouldRateLimit()) return;
+
+ HashMap<String, Long> timestamps = readTimestamps();
+ try {
+ tmpFileLock.lock();
+ Slog.i(TAG, "Filtering tombstone file: " + tombstone.getName());
+ // Create a temporary tombstone without memory sections.
+ filteredProto = createTempTombstoneWithoutMemory(tombstone);
+ Slog.i(TAG, "Generated tombstone file: " + filteredProto.getName());
+
+ if (recordFileTimestamp(tombstone, timestamps)) {
+ // We need to attach the count indicating the number of dropped dropbox entries
+ // due to rate limiting. Do this by enclosing the proto tombsstone in a
+ // container proto that has the dropped entry count and the proto tombstone as
+ // bytes (to avoid the complexity of reading and writing nested protos).
+ Slog.i(TAG, "Adding tombstone " + filteredProto.getName() + " to dropbox");
+ addAugmentedProtoToDropbox(filteredProto, db, rateLimitResult);
+ }
+ // Always add the text version of the tombstone to the DropBox, in order to
+ // match the previous behaviour.
+ Slog.i(TAG, "Adding text tombstone version of " + filteredProto.getName()
+ + " to dropbox");
+ addTextTombstoneFromProtoToDropbox(filteredProto, db, timestamps, rateLimitResult);
+
+ } catch (IOException | ProtoParseException e) {
+ Slog.e(TAG, "Failed to log tombstone '" + tombstone.getName()
+ + "' to DropBox. Error during processing or writing: " + e.getMessage(), e);
+ } finally {
+ if (filteredProto != null) {
+ filteredProto.delete();
+ }
+ tmpFileLock.unlock();
+ }
+ writeTimestamps(timestamps);
+ }
+
+ /**
+ * Creates a temporary tombstone file by filtering out memory mapping fields.
+ * This ensures that the unneeded memory mapping data is removed from the tombstone
+ * before adding it to Dropbox
+ *
+ * @param tombstone the original tombstone file to process
+ * @return a temporary file containing the filtered tombstone data
+ * @throws IOException if an I/O error occurs during processing
+ */
+ private static File createTempTombstoneWithoutMemory(File tombstone) throws IOException {
+ // Process the proto tombstone file and write it to a temporary file
+ File tombstoneProto =
+ File.createTempFile(tombstone.getName(), ".pb.tmp", TOMBSTONE_TMP_DIR);
+ ProtoFieldFilter protoFilter =
+ new ProtoFieldFilter(fieldNumber -> fieldNumber != (int) Tombstone.MEMORY_MAPPINGS);
+
+ try (FileInputStream fis = new FileInputStream(tombstone);
+ BufferedInputStream bis = new BufferedInputStream(fis);
+ FileOutputStream fos = new FileOutputStream(tombstoneProto);
+ BufferedOutputStream bos = new BufferedOutputStream(fos)) {
+ protoFilter.filter(bis, bos);
+ return tombstoneProto;
+ }
+ }
+
+ private static void addTextTombstoneFromProtoToDropbox(File tombstone, DropBoxManager db,
+ HashMap<String, Long> timestamps, DropboxRateLimiter.RateLimitResult rateLimitResult) {
+ File tombstoneTextFile = null;
+
+ try {
+ tombstoneTextFile = File.createTempFile(tombstone.getName(),
+ ".pb.txt.tmp", TOMBSTONE_TMP_DIR);
+
+ // Create a ProcessBuilder to execute pbtombstone
+ ProcessBuilder pb = new ProcessBuilder("/system/bin/pbtombstone", tombstone.getPath());
+ pb.redirectOutput(tombstoneTextFile);
+ Process process = pb.start();
+
+ // Wait 10 seconds for the process to complete
+ if (!process.waitFor(10, TimeUnit.SECONDS)) {
+ Slog.e(TAG, "pbtombstone timed out");
+ process.destroyForcibly();
+ return;
+ }
+
+ int exitCode = process.exitValue();
+ if (exitCode != 0) {
+ Slog.e(TAG, "pbtombstone failed with exit code " + exitCode);
+ } else {
+ final String headers = getBootHeadersToLogAndUpdate()
+ + rateLimitResult.createHeader();
+ addFileToDropBox(db, timestamps, headers, tombstoneTextFile.getPath(), LOG_SIZE,
+ TAG_TOMBSTONE);
+ }
+ } catch (IOException | InterruptedException e) {
+ Slog.e(TAG, "Failed to process tombstone with pbtombstone", e);
+ } finally {
+ if (tombstoneTextFile != null) {
+ tombstoneTextFile.delete();
+ }
+ }
+ }
+
private static void addAugmentedProtoToDropbox(
File tombstone, DropBoxManager db,
DropboxRateLimiter.RateLimitResult rateLimitResult) throws IOException {
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index e25ea4b43827..d1f07cb8ae23 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -90,6 +90,8 @@ class InputSettingsObserver extends ContentObserver {
(reason) -> updateTouchpadRightClickZoneEnabled()),
Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_SYSTEM_GESTURES),
(reason) -> updateTouchpadSystemGesturesEnabled()),
+ Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_ACCELERATION_ENABLED),
+ (reason) -> updateTouchpadAccelerationEnabled()),
Map.entry(Settings.System.getUriFor(Settings.System.SHOW_TOUCHES),
(reason) -> updateShowTouches()),
Map.entry(Settings.System.getUriFor(Settings.System.POINTER_LOCATION),
@@ -241,6 +243,11 @@ class InputSettingsObserver extends ContentObserver {
mNative.setTouchpadSystemGesturesEnabled(InputSettings.useTouchpadSystemGestures(mContext));
}
+ private void updateTouchpadAccelerationEnabled() {
+ mNative.setTouchpadAccelerationEnabled(
+ InputSettings.isTouchpadAccelerationEnabled(mContext));
+ }
+
private void updateShowTouches() {
mNative.setShowTouches(getBoolean(Settings.System.SHOW_TOUCHES, false));
}
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 4d38c8401e2d..f34338a397db 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -158,6 +158,8 @@ interface NativeInputManagerService {
void setTouchpadSystemGesturesEnabled(boolean enabled);
+ void setTouchpadAccelerationEnabled(boolean enabled);
+
void setShowTouches(boolean enabled);
void setNonInteractiveDisplays(int[] displayIds);
@@ -463,6 +465,9 @@ interface NativeInputManagerService {
public native void setTouchpadSystemGesturesEnabled(boolean enabled);
@Override
+ public native void setTouchpadAccelerationEnabled(boolean enabled);
+
+ @Override
public native void setShowTouches(boolean enabled);
@Override
diff --git a/services/core/java/com/android/server/os/NativeTombstoneManager.java b/services/core/java/com/android/server/os/NativeTombstoneManager.java
index f23d7823be94..33c122964d77 100644
--- a/services/core/java/com/android/server/os/NativeTombstoneManager.java
+++ b/services/core/java/com/android/server/os/NativeTombstoneManager.java
@@ -137,16 +137,26 @@ public final class NativeTombstoneManager {
return;
}
- String processName = "UNKNOWN";
final boolean isProtoFile = filename.endsWith(".pb");
- File protoPath = isProtoFile ? path : new File(path.getAbsolutePath() + ".pb");
- Optional<TombstoneFile> parsedTombstone = handleProtoTombstone(protoPath, isProtoFile);
- if (parsedTombstone.isPresent()) {
- processName = parsedTombstone.get().getProcessName();
+ // Only process the pb tombstone output, the text version will be generated in
+ // BootReceiver.filterAndAddTombstoneToDropBox through pbtombstone
+ if (Flags.protoTombstone() && !isProtoFile) {
+ return;
}
- BootReceiver.addTombstoneToDropBox(mContext, path, isProtoFile, processName, mTmpFileLock);
+ File protoPath = isProtoFile ? path : new File(path.getAbsolutePath() + ".pb");
+
+ final String processName = handleProtoTombstone(protoPath, isProtoFile)
+ .map(TombstoneFile::getProcessName)
+ .orElse("UNKNOWN");
+
+ if (Flags.protoTombstone()) {
+ BootReceiver.filterAndAddTombstoneToDropBox(mContext, path, processName, mTmpFileLock);
+ } else {
+ BootReceiver.addTombstoneToDropBox(mContext, path, isProtoFile,
+ processName, mTmpFileLock);
+ }
// TODO(b/339371242): An optimizer on WearOS is misbehaving and this member is being garbage
// collected as it's never referenced inside this class outside of the constructor. But,
// it's a file watcher, and needs to stay alive to do its job. So, add a cheap check here to
diff --git a/services/core/java/com/android/server/os/core_os_flags.aconfig b/services/core/java/com/android/server/os/core_os_flags.aconfig
index efdc9b8c164f..5e35cf5f02d3 100644
--- a/services/core/java/com/android/server/os/core_os_flags.aconfig
+++ b/services/core/java/com/android/server/os/core_os_flags.aconfig
@@ -3,7 +3,7 @@ container: "system"
flag {
name: "proto_tombstone"
- namespace: "proto_tombstone_ns"
+ namespace: "stability"
description: "Use proto tombstones as source of truth for adding to dropbox"
bug: "323857385"
}
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index ad19b9a44670..a1755e4d9d3b 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -52,6 +52,7 @@ import static android.window.WindowContainerTransaction.Change.CHANGE_FOCUSABLE;
import static android.window.WindowContainerTransaction.Change.CHANGE_FORCE_TRANSLUCENT;
import static android.window.WindowContainerTransaction.Change.CHANGE_HIDDEN;
import static android.window.WindowContainerTransaction.Change.CHANGE_RELATIVE_BOUNDS;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_KEYGUARD_STATE;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_INSETS_FRAME_PROVIDER;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION;
@@ -1131,6 +1132,23 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
}
break;
}
+ case HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK: {
+ final WindowContainer wc = WindowContainer.fromBinder(hop.getContainer());
+ if (wc == null || wc.asTask() == null || !wc.isAttached()
+ || !wc.asTask().isRootTask() || !wc.asTask().mCreatedByOrganizer) {
+ Slog.e(TAG, "Attempt to remove invalid task: " + wc);
+ break;
+ }
+ final Task task = wc.asTask();
+ if (task.isVisibleRequested() || task.isVisible()) {
+ effects |= TRANSACT_EFFECTS_LIFECYCLE;
+ }
+ // Removes its leaves, but not itself.
+ mService.mTaskSupervisor.removeRootTask(task);
+ // Now that the root has no leaves, remove it too. .
+ task.remove(true /* withTransition */, "remove-root-task-through-hierarchyOp");
+ break;
+ }
case HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT: {
final WindowContainer wc = WindowContainer.fromBinder(hop.getContainer());
if (wc == null || !wc.isAttached()) {
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 911c686c711f..e1f3f0ef5615 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -357,6 +357,7 @@ public:
void setTouchpadRightClickZoneEnabled(bool enabled);
void setTouchpadThreeFingerTapShortcutEnabled(bool enabled);
void setTouchpadSystemGesturesEnabled(bool enabled);
+ void setTouchpadAccelerationEnabled(bool enabled);
void setInputDeviceEnabled(uint32_t deviceId, bool enabled);
void setShowTouches(bool enabled);
void setNonInteractiveDisplays(const std::set<ui::LogicalDisplayId>& displayIds);
@@ -540,6 +541,10 @@ private:
// True to enable system gestures (three- and four-finger swipes) on touchpads.
bool touchpadSystemGesturesEnabled{true};
+ // True if the speed of the pointer will increase as the user moves
+ // their finger faster on the touchpad.
+ bool touchpadAccelerationEnabled{true};
+
// True if a pointer icon should be shown for stylus pointers.
bool stylusPointerIconEnabled{false};
@@ -869,6 +874,7 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon
outConfig->touchpadThreeFingerTapShortcutEnabled =
mLocked.touchpadThreeFingerTapShortcutEnabled;
outConfig->touchpadSystemGesturesEnabled = mLocked.touchpadSystemGesturesEnabled;
+ outConfig->touchpadAccelerationEnabled = mLocked.touchpadAccelerationEnabled;
outConfig->disabledDevices = mLocked.disabledInputDevices;
@@ -1666,6 +1672,21 @@ void NativeInputManager::setTouchpadSystemGesturesEnabled(bool enabled) {
InputReaderConfiguration::Change::TOUCHPAD_SETTINGS);
}
+void NativeInputManager::setTouchpadAccelerationEnabled(bool enabled) {
+ { // acquire lock
+ std::scoped_lock _l(mLock);
+
+ if (mLocked.touchpadAccelerationEnabled == enabled) {
+ return;
+ }
+
+ mLocked.touchpadAccelerationEnabled = enabled;
+ } // release lock
+
+ mInputManager->getReader().requestRefreshConfiguration(
+ InputReaderConfiguration::Change::TOUCHPAD_SETTINGS);
+}
+
void NativeInputManager::setInputDeviceEnabled(uint32_t deviceId, bool enabled) {
bool refresh = false;
@@ -2644,6 +2665,13 @@ static void nativeSetTouchpadSystemGesturesEnabled(JNIEnv* env, jobject nativeIm
im->setTouchpadSystemGesturesEnabled(enabled);
}
+static void nativeSetTouchpadAccelerationEnabled(JNIEnv* env, jobject nativeImplObj,
+ jboolean enabled) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+
+ im->setTouchpadAccelerationEnabled(enabled);
+}
+
static void nativeSetShowTouches(JNIEnv* env, jobject nativeImplObj, jboolean enabled) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -3354,6 +3382,7 @@ static const JNINativeMethod gInputManagerMethods[] = {
{"setTouchpadThreeFingerTapShortcutEnabled", "(Z)V",
(void*)nativeSetTouchpadThreeFingerTapShortcutEnabled},
{"setTouchpadSystemGesturesEnabled", "(Z)V", (void*)nativeSetTouchpadSystemGesturesEnabled},
+ {"setTouchpadAccelerationEnabled", "(Z)V", (void*)nativeSetTouchpadAccelerationEnabled},
{"setShowTouches", "(Z)V", (void*)nativeSetShowTouches},
{"setNonInteractiveDisplays", "([I)V", (void*)nativeSetNonInteractiveDisplays},
{"reloadCalibration", "()V", (void*)nativeReloadCalibration},
diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java
index e8e1dacd6fcf..8ce05e2fa115 100644
--- a/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java
@@ -40,6 +40,7 @@ import com.android.modules.utils.testing.TestableDeviceConfig;
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
@@ -329,6 +330,7 @@ public class ScreenUndimDetectorTest {
}
}
+ @Ignore("b/387389929")
@Test
public void recordScreenPolicy_otherTransitions_doesNotReset() {
DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE,
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java
index e9ece5dbdcc4..369600c3f8d7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java
@@ -79,6 +79,34 @@ public class WindowContainerTransactionTests extends WindowTestsBase {
}
@Test
+ public void testRemoveRootTask() {
+ final Task rootTask = createTask(mDisplayContent);
+ final Task task = createTaskInRootTask(rootTask, 0 /* userId */);
+ final ActivityRecord activity = createActivityRecord(mDisplayContent, task);
+ final TaskDisplayArea taskDisplayArea = mDisplayContent.getDefaultTaskDisplayArea();
+
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ WindowContainerToken token = rootTask.getTaskInfo().token;
+ wct.removeTask(token);
+ applyTransaction(wct);
+
+ // There is still an activity to be destroyed, so the task is not removed immediately.
+ assertNotNull(task.getParent());
+ assertTrue(rootTask.hasChild());
+ assertTrue(task.hasChild());
+ assertTrue(activity.finishing);
+
+ activity.destroyed("testRemoveRootTask");
+ // Assert that the container was removed after the activity is destroyed.
+ assertNull(task.getParent());
+ assertEquals(0, task.getChildCount());
+ assertNull(activity.getParent());
+ assertNull(taskDisplayArea.getTask(task1 -> task1.mTaskId == rootTask.mTaskId));
+ verify(mAtm.getLockTaskController(), atLeast(1)).clearLockedTask(task);
+ verify(mAtm.getLockTaskController(), atLeast(1)).clearLockedTask(rootTask);
+ }
+
+ @Test
public void testDesktopMode_tasksAreBroughtToFront() {
final TestDesktopOrganizer desktopOrganizer = new TestDesktopOrganizer(mAtm);
TaskDisplayArea tda = desktopOrganizer.mDefaultTDA;
diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java
index a52614d5cda1..531f51604507 100644
--- a/telecomm/java/android/telecom/Call.java
+++ b/telecomm/java/android/telecom/Call.java
@@ -52,6 +52,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
* Represents an ongoing phone call that the in-call app should present to the user.
*/
public final class Call {
+ private static final String LOG_TAG = "TelecomCall";
+
/**
* The state of a {@code Call} when newly created.
*/
@@ -2912,6 +2914,11 @@ public final class Call {
}
} catch (BadParcelableException e) {
return false;
+ } catch (ClassCastException e) {
+ Log.e(LOG_TAG, e, "areBundlesEqual: failure comparing bundle key %s", key);
+ // until we know what is causing this, we should rethrow -- this is still not
+ // expected.
+ throw e;
}
}
}