diff options
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; } } } |