diff options
62 files changed, 1448 insertions, 527 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index db701546ba62..0d5ec199b953 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2938,6 +2938,14 @@ package android.app.smartspace.uitemplatedata { } +package android.app.supervision { + + @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager { + method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isSupervisionEnabled(); + } + +} + package android.app.time { public final class Capabilities { @@ -3801,6 +3809,7 @@ package android.content { field public static final String SHARED_CONNECTIVITY_SERVICE = "shared_connectivity"; field public static final String SMARTSPACE_SERVICE = "smartspace"; field public static final String STATS_MANAGER = "stats"; + field @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public static final String SUPERVISION_SERVICE = "supervision"; field public static final String SYSTEM_CONFIG_SERVICE = "system_config"; field public static final String SYSTEM_UPDATE_SERVICE = "system_update"; field @FlaggedApi("com.android.net.thread.platform.flags.thread_enabled_platform") public static final String THREAD_NETWORK_SERVICE = "thread_network"; diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 85b65bb6605e..975c2c27cb22 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -850,6 +850,14 @@ package android.app.prediction { } +package android.app.supervision { + + @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager { + method public void setSupervisionEnabled(boolean); + } + +} + package android.app.usage { public class StorageStatsManager { diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl index e583302e4d3b..2f67a8abcd17 100644 --- a/core/java/android/app/supervision/ISupervisionManager.aidl +++ b/core/java/android/app/supervision/ISupervisionManager.aidl @@ -16,11 +16,14 @@ package android.app.supervision; +import android.content.Intent; + /** * Internal IPC interface to the supervision service. * {@hide} */ interface ISupervisionManager { + Intent createConfirmSupervisionCredentialsIntent(); boolean isSupervisionEnabledForUser(int userId); void setSupervisionEnabledForUser(int userId, boolean enabled); String getActiveSupervisionAppPackage(int userId); diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java index d30705536045..0270edf080a9 100644 --- a/core/java/android/app/supervision/SupervisionManager.java +++ b/core/java/android/app/supervision/SupervisionManager.java @@ -16,13 +16,22 @@ package android.app.supervision; +import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import static android.Manifest.permission.MANAGE_USERS; +import static android.Manifest.permission.QUERY_USERS; + +import android.annotation.FlaggedApi; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.annotation.UserHandleAware; import android.annotation.UserIdInt; +import android.app.supervision.flags.Flags; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.content.Intent; import android.os.RemoteException; /** @@ -31,6 +40,8 @@ import android.os.RemoteException; * @hide */ @SystemService(Context.SUPERVISION_SERVICE) +@SystemApi +@FlaggedApi(Flags.FLAG_SUPERVISION_MANAGER_APIS) public class SupervisionManager { private final Context mContext; @Nullable private final ISupervisionManager mService; @@ -47,7 +58,8 @@ public class SupervisionManager { * * @hide */ - public static final String ACTION_ENABLE_SUPERVISION = "android.app.action.ENABLE_SUPERVISION"; + public static final String ACTION_ENABLE_SUPERVISION = + "android.app.supervision.action.ENABLE_SUPERVISION"; /** * Activity action: ask the human user to disable supervision for this user. Only the app that @@ -62,7 +74,7 @@ public class SupervisionManager { * @hide */ public static final String ACTION_DISABLE_SUPERVISION = - "android.app.action.DISABLE_SUPERVISION"; + "android.app.supervision.action.DISABLE_SUPERVISION"; /** @hide */ @UnsupportedAppUsage @@ -72,11 +84,46 @@ public class SupervisionManager { } /** + * Creates an {@link Intent} that can be used with {@link Context#startActivity(Intent)} to + * launch the activity to verify supervision credentials. + * + * <p>A valid {@link Intent} is always returned if supervision is enabled at the time this API + * is called, the launched activity still need to perform validity checks as the supervision + * state can change when the activity is launched. A null intent is returned if supervision is + * disabled at the time of this API call. + * + * <p>A result code of {@link android.app.Activity#RESULT_OK} indicates successful verification + * of the supervision credentials. + * + * @hide + */ + @RequiresPermission(value = android.Manifest.permission.QUERY_USERS) + @Nullable + public Intent createConfirmSupervisionCredentialsIntent() { + if (mService != null) { + try { + Intent result = mService.createConfirmSupervisionCredentialsIntent(); + if (result != null) { + result.prepareToEnterProcess( + Intent.LOCAL_FLAG_FROM_SYSTEM, mContext.getAttributionSource()); + } + return result; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return null; + } + + /** * Returns whether the device is supervised. * * @hide */ - @UserHandleAware + @SystemApi + @FlaggedApi(Flags.FLAG_SUPERVISION_MANAGER_APIS) + @RequiresPermission(anyOf = {MANAGE_USERS, QUERY_USERS}) + @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS) public boolean isSupervisionEnabled() { return isSupervisionEnabledForUser(mContext.getUserId()); } @@ -84,14 +131,10 @@ public class SupervisionManager { /** * Returns whether the device is supervised. * - * <p>The caller must be from the same user as the target or hold the {@link - * android.Manifest.permission#INTERACT_ACROSS_USERS} permission. - * * @hide */ - @RequiresPermission( - value = android.Manifest.permission.INTERACT_ACROSS_USERS, - conditional = true) + @RequiresPermission(anyOf = {MANAGE_USERS, QUERY_USERS}) + @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS) public boolean isSupervisionEnabledForUser(@UserIdInt int userId) { if (mService != null) { try { @@ -108,7 +151,8 @@ public class SupervisionManager { * * @hide */ - @UserHandleAware + @TestApi + @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS) public void setSupervisionEnabled(boolean enabled) { setSupervisionEnabledForUser(mContext.getUserId(), enabled); } @@ -116,14 +160,9 @@ public class SupervisionManager { /** * Sets whether the device is supervised for a given user. * - * <p>The caller must be from the same user as the target or hold the {@link - * android.Manifest.permission#INTERACT_ACROSS_USERS} permission. - * * @hide */ - @RequiresPermission( - value = android.Manifest.permission.INTERACT_ACROSS_USERS, - conditional = true) + @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS) public void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) { if (mService != null) { try { diff --git a/core/java/android/app/supervision/flags.aconfig b/core/java/android/app/supervision/flags.aconfig index 232883cbfe00..94de03877fd7 100644 --- a/core/java/android/app/supervision/flags.aconfig +++ b/core/java/android/app/supervision/flags.aconfig @@ -64,3 +64,11 @@ flag { description: "Flag that enables the Supervision pin recovery screen with Supervision settings entry point" bug: "390500290" } + +flag { + name: "supervision_manager_apis" + is_exported: true + namespace: "supervision" + description: "Flag that enables system APIs in Supervision Manager" + bug: "382034839" +} diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 3391e79b2ae4..55d78f9b8c48 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -17,8 +17,8 @@ package android.content; import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER; -import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS; import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE; +import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS; import static android.security.Flags.FLAG_SECURE_LOCKDOWN; import android.annotation.AttrRes; @@ -6858,6 +6858,8 @@ public abstract class Context { * @see android.app.supervision.SupervisionManager * @hide */ + @SystemApi + @FlaggedApi(android.app.supervision.flags.Flags.FLAG_SUPERVISION_MANAGER_APIS) public static final String SUPERVISION_SERVICE = "supervision"; /** diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index ca0959af3ff8..231aa6816908 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -1599,7 +1599,6 @@ public final class Display { mGlobal.registerDisplayListener(toRegister, executor, DisplayManagerGlobal .INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED - | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE | DisplayManagerGlobal .INTERNAL_EVENT_FLAG_DISPLAY_HDR_SDR_RATIO_CHANGED, ActivityThread.currentPackageName()); diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java index 73cd5ecd39ef..df680c054f56 100644 --- a/core/java/android/view/NotificationHeaderView.java +++ b/core/java/android/view/NotificationHeaderView.java @@ -32,7 +32,6 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; -import android.util.TypedValue; import android.widget.FrameLayout; import android.widget.RelativeLayout; import android.widget.RemoteViews; @@ -266,20 +265,14 @@ public class NotificationHeaderView extends RelativeLayout { ? R.style.TextAppearance_DeviceDefault_Notification_Title : R.style.TextAppearance_DeviceDefault_Notification_Info; // Most of the time, we're showing text in the minimized state - if (findViewById(R.id.header_text) instanceof TextView headerText) { - headerText.setTextAppearance(styleResId); - if (notificationsRedesignTemplates()) { - // TODO: b/378660052 - When inlining the redesign flag, this should be updated - // directly in TextAppearance_DeviceDefault_Notification_Title so we won't need to - // override it here. - float textSize = getContext().getResources().getDimension( - R.dimen.notification_2025_title_text_size); - headerText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); - } + View headerText = findViewById(R.id.header_text); + if (headerText instanceof TextView) { + ((TextView) headerText).setTextAppearance(styleResId); } // If there's no summary or text, we show the app name instead of nothing - if (findViewById(R.id.app_name_text) instanceof TextView appNameText) { - appNameText.setTextAppearance(styleResId); + View appNameText = findViewById(R.id.app_name_text); + if (appNameText instanceof TextView) { + ((TextView) appNameText).setTextAppearance(styleResId); } } diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index 2ba6bc4912c3..b679688959b1 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -664,14 +664,16 @@ static void android_media_AudioSystem_vol_range_init_req_callback() static jint android_media_AudioSystem_setDeviceConnectionState(JNIEnv *env, jobject thiz, jint state, jobject jParcel, - jint codec) { + jint codec, jboolean deviceSwitch) { int status; if (Parcel *parcel = parcelForJavaObject(env, jParcel); parcel != nullptr) { android::media::audio::common::AudioPort port{}; if (status_t statusOfParcel = port.readFromParcel(parcel); statusOfParcel == OK) { - status = check_AudioSystem_Command( - AudioSystem::setDeviceConnectionState(static_cast<audio_policy_dev_state_t>(state), - port, static_cast<audio_format_t>(codec))); + status = check_AudioSystem_Command( + AudioSystem::setDeviceConnectionState(static_cast<audio_policy_dev_state_t>( + state), + port, static_cast<audio_format_t>(codec), + deviceSwitch)); } else { ALOGE("Failed to read from parcel: %s", statusToString(statusOfParcel).c_str()); status = kAudioStatusError; @@ -3457,7 +3459,7 @@ static const JNINativeMethod gMethods[] = { MAKE_AUDIO_SYSTEM_METHOD(newAudioSessionId), MAKE_AUDIO_SYSTEM_METHOD(newAudioPlayerId), MAKE_AUDIO_SYSTEM_METHOD(newAudioRecorderId), - MAKE_JNI_NATIVE_METHOD("setDeviceConnectionState", "(ILandroid/os/Parcel;I)I", + MAKE_JNI_NATIVE_METHOD("setDeviceConnectionState", "(ILandroid/os/Parcel;IZ)I", android_media_AudioSystem_setDeviceConnectionState), MAKE_AUDIO_SYSTEM_METHOD(getDeviceConnectionState), MAKE_AUDIO_SYSTEM_METHOD(handleDeviceConfigChange), diff --git a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp index e0cc055a62a6..c4259f41e380 100644 --- a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp +++ b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp @@ -266,16 +266,24 @@ class NativeCommandBuffer { } // Picky version of atoi(). No sign or unexpected characters allowed. Return -1 on failure. static int digitsVal(char* start, char* end) { + constexpr int vmax = std::numeric_limits<int>::max(); int result = 0; - if (end - start > 6) { - return -1; - } for (char* dp = start; dp < end; ++dp) { if (*dp < '0' || *dp > '9') { - ALOGW("Argument failed integer format check"); + ALOGW("Argument contains non-integer characters"); + return -1; + } + int digit = *dp - '0'; + if (result > vmax / 10) { + ALOGW("Argument exceeds int limit"); + return -1; + } + result *= 10; + if (result > vmax - digit) { + ALOGW("Argument exceeds int limit"); return -1; } - result = 10 * result + (*dp - '0'); + result += digit; } return result; } diff --git a/core/res/res/layout/notification_2025_conversation_header.xml b/core/res/res/layout/notification_2025_conversation_header.xml index 75bd244cbbf4..1bde17358825 100644 --- a/core/res/res/layout/notification_2025_conversation_header.xml +++ b/core/res/res/layout/notification_2025_conversation_header.xml @@ -29,7 +29,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" + android:textSize="16sp" android:singleLine="true" android:layout_weight="1" /> diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml index 054583297d37..d29b7af9e24e 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_base.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml @@ -102,7 +102,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml index 9959b666b3bf..5beab508aecf 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_media.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml @@ -104,7 +104,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml index 85ca124de8ff..d7c3263904d4 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml @@ -130,7 +130,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> diff --git a/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml b/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml index 11fc48668ad7..52bc7b8ea3bb 100644 --- a/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml +++ b/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml @@ -69,7 +69,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml b/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml index bf70a5eff47e..cf9ff6bef6f8 100644 --- a/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml +++ b/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml @@ -90,7 +90,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 1a311d572e0b..2188469bdf03 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2643,6 +2643,15 @@ <!-- MMS user agent prolfile url --> <string name="config_mms_user_agent_profile_url" translatable="false"></string> + <!-- The default list of possible CMF Names|Style|ColorSource. This array can be + overridden device-specific resources. A wildcard (fallback) must be supplied. + Name - Read from `ro.boot.hardware.color` sysprop. Fallback (*) required. + Styles - frameworks/libs/systemui/monet/src/com/android/systemui/monet/Style.java + Color - `home_wallpaper` (for color extraction) or a hexadecimal int (#FFcc99) --> + <string-array name="theming_defaults"> + <item>*|TONAL_SPOT|home_wallpaper</item> + </string-array> + <!-- National Language Identifier codes for the following two config items. (from 3GPP TS 23.038 V9.1.1 Table 6.2.1.2.4.1): 0 - reserved diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index e9d87e4b5f5b..9acb2427aaab 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -580,9 +580,6 @@ <dimen name="notification_text_size">14sp</dimen> <!-- Size of notification text titles (see TextAppearance.StatusBar.EventContent.Title) --> <dimen name="notification_title_text_size">14sp</dimen> - <!-- Size of notification text titles, 2025 redesign version (see TextAppearance.StatusBar.EventContent.Title) --> - <!-- TODO: b/378660052 - When inlining the redesign flag, this should be updated directly in TextAppearance.DeviceDefault.Notification.Title --> - <dimen name="notification_2025_title_text_size">16sp</dimen> <!-- Size of big notification text titles (see TextAppearance.StatusBar.EventContent.BigTitle) --> <dimen name="notification_big_title_text_size">16sp</dimen> <!-- Size of smaller notification text (see TextAppearance.StatusBar.EventContent.Line2, Info, Time) --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index c62732d36038..ffcfce9c420e 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -575,7 +575,6 @@ <java-symbol type="dimen" name="notification_text_size" /> <java-symbol type="dimen" name="notification_title_text_size" /> <java-symbol type="dimen" name="notification_subtext_size" /> - <java-symbol type="dimen" name="notification_2025_title_text_size" /> <java-symbol type="dimen" name="notification_top_pad" /> <java-symbol type="dimen" name="notification_top_pad_narrow" /> <java-symbol type="dimen" name="notification_top_pad_large_text" /> @@ -5904,6 +5903,9 @@ <java-symbol type="drawable" name="ic_notification_summarization" /> <java-symbol type="dimen" name="notification_collapsed_height_with_summarization" /> + <!-- Device CMF Theming Settings --> + <java-symbol type="array" name="theming_defaults" /> + <!-- Advanced Protection Service USB feature --> <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_title" /> <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_text" /> diff --git a/graphics/java/android/graphics/FrameInfo.java b/graphics/java/android/graphics/FrameInfo.java index 7d236d203201..3b8f46630344 100644 --- a/graphics/java/android/graphics/FrameInfo.java +++ b/graphics/java/android/graphics/FrameInfo.java @@ -93,10 +93,12 @@ public final class FrameInfo { // Interval between two consecutive frames public static final int FRAME_INTERVAL = 11; + // Workload target deadline for a frame + public static final int WORKLOAD_TARGET = 12; + // Must be the last one // This value must be in sync with `UI_THREAD_FRAME_INFO_SIZE` in FrameInfo.h - // In calculating size, + 1 for Flags, and + 1 for WorkloadTarget from FrameInfo.h - private static final int FRAME_INFO_SIZE = FRAME_INTERVAL + 2; + private static final int FRAME_INFO_SIZE = WORKLOAD_TARGET + 1; /** checkstyle */ public void setVsync(long intendedVsync, long usedVsync, long frameTimelineVsyncId, @@ -108,6 +110,7 @@ public final class FrameInfo { frameInfo[FRAME_DEADLINE] = frameDeadline; frameInfo[FRAME_START_TIME] = frameStartTime; frameInfo[FRAME_INTERVAL] = frameInterval; + frameInfo[WORKLOAD_TARGET] = frameDeadline - intendedVsync; } /** checkstyle */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt new file mode 100644 index 000000000000..a1d700af5569 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.window.WindowContainerTransaction +import com.android.wm.shell.dagger.WMSingleton +import java.util.function.Supplier +import javax.inject.Inject + +/** + * An Injectable [Supplier<WindowContainerTransaction>]. This can be used in place of kotlin default + * parameters values [builder = ::WindowContainerTransaction] which requires the + * [@JvmOverloads] annotation to make this available in Java. + * This can be used every time a component needs the dependency to the default [Supplier] for + * [WindowContainerTransaction]s. + */ +@WMSingleton +class WindowContainerTransactionSupplier @Inject constructor( +) : Supplier<WindowContainerTransaction> { + override fun get(): WindowContainerTransaction = WindowContainerTransaction() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt new file mode 100644 index 000000000000..bdffcf51e7d4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox.events + +import android.graphics.Rect +import android.view.GestureDetector +import android.view.MotionEvent +import android.window.WindowContainerToken +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY + +/** + * [GestureDetector.SimpleOnGestureListener] implementation which receives events from the + * Letterbox Input surface, understands the type of event and filter them based on the current + * letterbox position. + */ +class ReachabilityGestureListener( + private val taskId: Int, + private val token: WindowContainerToken?, + private val transitions: Transitions, + private val animationHandler: Transitions.TransitionHandler, + private val wctSupplier: WindowContainerTransactionSupplier +) : GestureDetector.SimpleOnGestureListener() { + + // The current letterbox bounds. Double tap events are ignored when happening in these bounds. + private val activityBounds = Rect() + + override fun onDoubleTap(e: MotionEvent): Boolean { + val x = e.rawX.toInt() + val y = e.rawY.toInt() + if (!activityBounds.contains(x, y)) { + val wct = wctSupplier.get().apply { + setReachabilityOffset(token!!, taskId, x, y) + } + transitions.startTransition( + TRANSIT_MOVE_LETTERBOX_REACHABILITY, + wct, + animationHandler + ) + return true + } + return false + } + + /** + * Updates the bounds for the letterboxed activity. + */ + fun updateActivityBounds(newActivityBounds: Rect) { + activityBounds.set(newActivityBounds) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt new file mode 100644 index 000000000000..5e9fe09bc840 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox.events + +import android.window.WindowContainerToken +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.dagger.WMSingleton +import com.android.wm.shell.transition.Transitions +import javax.inject.Inject + +/** + * A Factory for [ReachabilityGestureListener]. + */ +@WMSingleton +class ReachabilityGestureListenerFactory @Inject constructor( + private val transitions: Transitions, + private val animationHandler: Transitions.TransitionHandler, + private val wctSupplier: WindowContainerTransactionSupplier +) { + /** + * @return a [ReachabilityGestureListener] implementation to listen to double tap events and + * creating the related [WindowContainerTransaction] to handle the transition. + */ + fun createReachabilityGestureListener( + taskId: Int, + token: WindowContainerToken? + ): ReachabilityGestureListener = + ReachabilityGestureListener(taskId, token, transitions, animationHandler, wctSupplier) +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt new file mode 100644 index 000000000000..c91ef5e6b868 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.testing.AndroidTestingRunner +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [WindowContainerTransactionSupplier]. + * + * Build/Install/Run: + * atest WMShellUnitTests:WindowContainerTransactionSupplierTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class WindowContainerTransactionSupplierTest { + + @Test + fun `WindowContainerTransactionSupplier supplies a WindowContainerTransaction`() { + val supplier = WindowContainerTransactionSupplier() + SuppliersUtilsTest.assertSupplierProvidesValue(supplier) { + it is WindowContainerTransaction + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt new file mode 100644 index 000000000000..a5f6ced20dc0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox.events + +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY +import java.util.function.Consumer +import kotlin.test.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +/** + * Tests for [ReachabilityGestureListenerFactory]. + * + * Build/Install/Run: + * atest WMShellUnitTests:ReachabilityGestureListenerFactoryTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ReachabilityGestureListenerFactoryTest : ShellTestCase() { + + @Test + fun `When invoked a ReachabilityGestureListenerFactory is created`() { + runTestScenario { r -> + r.invokeCreate() + + r.checkReachabilityGestureListenerCreated() + } + } + + @Test + fun `Right parameters are used for creation`() { + runTestScenario { r -> + r.invokeCreate() + + r.checkRightParamsAreUsed() + } + } + + /** + * Runs a test scenario providing a Robot. + */ + fun runTestScenario(consumer: Consumer<ReachabilityGestureListenerFactoryRobotTest>) { + val robot = ReachabilityGestureListenerFactoryRobotTest() + consumer.accept(robot) + } + + class ReachabilityGestureListenerFactoryRobotTest { + + companion object { + @JvmStatic + private val TASK_ID = 1 + + @JvmStatic + private val TOKEN = mock<WindowContainerToken>() + } + + private val transitions: Transitions + private val animationHandler: Transitions.TransitionHandler + private val factory: ReachabilityGestureListenerFactory + private val wctSupplier: WindowContainerTransactionSupplier + private val wct: WindowContainerTransaction + private lateinit var obtainedResult: Any + + init { + transitions = mock<Transitions>() + animationHandler = mock<Transitions.TransitionHandler>() + wctSupplier = mock<WindowContainerTransactionSupplier>() + wct = mock<WindowContainerTransaction>() + doReturn(wct).`when`(wctSupplier).get() + factory = ReachabilityGestureListenerFactory(transitions, animationHandler, wctSupplier) + } + + fun invokeCreate(taskId: Int = TASK_ID, token: WindowContainerToken? = TOKEN) { + obtainedResult = factory.createReachabilityGestureListener(taskId, token) + } + + fun checkReachabilityGestureListenerCreated(expected: Boolean = true) { + assertEquals(expected, obtainedResult is ReachabilityGestureListener) + } + + fun checkRightParamsAreUsed(taskId: Int = TASK_ID, token: WindowContainerToken? = TOKEN) { + with(obtainedResult as ReachabilityGestureListener) { + // Click outside the bounds + updateActivityBounds(Rect(0, 0, 10, 20)) + onDoubleTap(motionEventAt(50f, 100f)) + // WindowContainerTransactionSupplier is invoked to create a + // WindowContainerTransaction + verify(wctSupplier).get() + // Verify the right params are passed to startAppCompatReachability() + verify(wct).setReachabilityOffset( + token!!, + taskId, + 50, + 100 + ) + // startTransition() is invoked on Transitions with the right parameters + verify(transitions).startTransition( + TRANSIT_MOVE_LETTERBOX_REACHABILITY, + wct, + animationHandler + ) + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt new file mode 100644 index 000000000000..bc10ea578ffb --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox.events + +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt +import com.android.wm.shell.compatui.letterbox.asMode +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY +import java.util.function.Consumer +import kotlin.test.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +/** + * Tests for [ReachabilityGestureListener]. + * + * Build/Install/Run: + * atest WMShellUnitTests:ReachabilityGestureListenerTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ReachabilityGestureListenerTest : ShellTestCase() { + + @Test + fun `Only events outside the bounds are handled`() { + runTestScenario { r -> + r.updateActivityBounds(Rect(0, 0, 100, 200)) + r.sendMotionEvent(50, 100) + + r.verifyReachabilityTransitionCreated(expected = false, 50, 100) + r.verifyReachabilityTransitionStarted(expected = false) + r.verifyEventIsHandled(expected = false) + + r.updateActivityBounds(Rect(0, 0, 10, 50)) + r.sendMotionEvent(50, 100) + + r.verifyReachabilityTransitionCreated(expected = true, 50, 100) + r.verifyReachabilityTransitionStarted(expected = true) + r.verifyEventIsHandled(expected = true) + } + } + + /** + * Runs a test scenario providing a Robot. + */ + fun runTestScenario(consumer: Consumer<ReachabilityGestureListenerRobotTest>) { + val robot = ReachabilityGestureListenerRobotTest() + consumer.accept(robot) + } + + class ReachabilityGestureListenerRobotTest( + taskId: Int = TASK_ID, + token: WindowContainerToken? = TOKEN + ) { + + companion object { + @JvmStatic + private val TASK_ID = 1 + + @JvmStatic + private val TOKEN = mock<WindowContainerToken>() + } + + private val reachabilityListener: ReachabilityGestureListener + private val transitions: Transitions + private val animationHandler: Transitions.TransitionHandler + private val wctSupplier: WindowContainerTransactionSupplier + private val wct: WindowContainerTransaction + private var eventHandled = false + + init { + transitions = mock<Transitions>() + animationHandler = mock<Transitions.TransitionHandler>() + wctSupplier = mock<WindowContainerTransactionSupplier>() + wct = mock<WindowContainerTransaction>() + doReturn(wct).`when`(wctSupplier).get() + reachabilityListener = + ReachabilityGestureListener( + taskId, + token, + transitions, + animationHandler, + wctSupplier + ) + } + + fun updateActivityBounds(activityBounds: Rect) { + reachabilityListener.updateActivityBounds(activityBounds) + } + + fun sendMotionEvent(x: Int, y: Int) { + eventHandled = reachabilityListener.onDoubleTap(motionEventAt(x.toFloat(), y.toFloat())) + } + + fun verifyReachabilityTransitionCreated( + expected: Boolean, + x: Int, + y: Int, + taskId: Int = TASK_ID, + token: WindowContainerToken? = TOKEN + ) { + verify(wct, expected.asMode()).setReachabilityOffset( + token!!, + taskId, + x, + y + ) + } + + fun verifyReachabilityTransitionStarted(expected: Boolean = true) { + verify(transitions, expected.asMode()).startTransition( + TRANSIT_MOVE_LETTERBOX_REACHABILITY, + wct, + animationHandler + ) + } + + fun verifyEventIsHandled(expected: Boolean) { + assertEquals(expected, eventHandled) + } + } +} diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index 12d7f33a0d51..e01cb928e369 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -1754,13 +1754,21 @@ public class AudioSystem @UnsupportedAppUsage public static int setDeviceConnectionState(AudioDeviceAttributes attributes, int state, int codecFormat) { + return setDeviceConnectionState(attributes, state, codecFormat, false /*deviceSwitch*/); + } + + /** + * @hide + */ + public static int setDeviceConnectionState(AudioDeviceAttributes attributes, int state, + int codecFormat, boolean deviceSwitch) { android.media.audio.common.AudioPort port = AidlConversion.api2aidl_AudioDeviceAttributes_AudioPort(attributes); Parcel parcel = Parcel.obtain(); port.writeToParcel(parcel, 0); parcel.setDataPosition(0); try { - return setDeviceConnectionState(state, parcel, codecFormat); + return setDeviceConnectionState(state, parcel, codecFormat, deviceSwitch); } finally { parcel.recycle(); } @@ -1769,7 +1777,10 @@ public class AudioSystem * @hide */ @UnsupportedAppUsage - public static native int setDeviceConnectionState(int state, Parcel parcel, int codecFormat); + public static native int setDeviceConnectionState(int state, Parcel parcel, int codecFormat, + boolean deviceSwitch); + + /** @hide */ @UnsupportedAppUsage public static native int getDeviceConnectionState(int device, String device_address); diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java index 0f24654879cd..021348153bb8 100644 --- a/media/java/android/media/RingtoneManager.java +++ b/media/java/android/media/RingtoneManager.java @@ -60,6 +60,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * RingtoneManager provides access to ringtones, notification, and other types @@ -810,9 +811,7 @@ public class RingtoneManager { // Don't set the stream type Ringtone ringtone = getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig, false); - if (Flags.enableRingtoneHapticsCustomization() - && Utils.isRingtoneVibrationSettingsSupported(context) - && Utils.hasVibration(ringtoneUri) && hasHapticChannels(ringtoneUri)) { + if (muteHapticChannelForVibration(context, ringtoneUri)) { audioAttributes = new AudioAttributes.Builder( audioAttributes).setHapticChannelsMuted(true).build(); } @@ -1305,4 +1304,19 @@ public class RingtoneManager { default: throw new IllegalArgumentException(); } } + + private static boolean muteHapticChannelForVibration(Context context, Uri ringtoneUri) { + final Uri vibrationUri = Utils.getVibrationUri(ringtoneUri); + // No vibration is specified + if (vibrationUri == null) { + return false; + } + // The user specified the synchronized pattern + if (Objects.equals(vibrationUri.toString(), Utils.SYNCHRONIZED_VIBRATION)) { + return false; + } + return Flags.enableRingtoneHapticsCustomization() + && Utils.isRingtoneVibrationSettingsSupported(context) + && hasHapticChannels(ringtoneUri); + } } diff --git a/media/java/android/media/Utils.java b/media/java/android/media/Utils.java index 11bd221ec696..d6e27b0ffa75 100644 --- a/media/java/android/media/Utils.java +++ b/media/java/android/media/Utils.java @@ -66,6 +66,8 @@ public class Utils { public static final String VIBRATION_URI_PARAM = "vibration_uri"; + public static final String SYNCHRONIZED_VIBRATION = "synchronized"; + /** * Sorts distinct (non-intersecting) range array in ascending order. * @throws java.lang.IllegalArgumentException if ranges are not distinct @@ -757,8 +759,8 @@ public class Utils { return null; } String filePath = vibrationUri.getPath(); - if (filePath == null) { - Log.w(TAG, "The file path is null."); + if (filePath == null || filePath.equals(Utils.SYNCHRONIZED_VIBRATION)) { + Log.w(TAG, "Ignore the vibration parsing for file:" + filePath); return null; } File vibrationFile = new File(filePath); diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 4b0400fb3441..91ec83690722 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -889,6 +889,9 @@ <!-- Preference category for monitoring debugging development settings. [CHAR LIMIT=25] --> <string name="debug_monitoring_category">Monitoring</string> + <!-- Preference category to alter window management settings, [CHAR LIMIT=50] --> + <string name="window_management_category">Window Management</string> + <!-- UI debug setting: always enable strict mode? [CHAR LIMIT=25] --> <string name="strict_mode">Strict mode enabled</string> <!-- UI debug setting: show strict mode summary [CHAR LIMIT=50] --> diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS index 236654deefb5..f5c0233d56b1 100644 --- a/packages/SystemUI/OWNERS +++ b/packages/SystemUI/OWNERS @@ -8,7 +8,6 @@ achalke@google.com acul@google.com adamcohen@google.com aioana@google.com -alexchau@google.com alexflo@google.com andonian@google.com amiko@google.com @@ -91,10 +90,8 @@ rahulbanerjee@google.com rgl@google.com roosa@google.com saff@google.com -samcackett@google.com santie@google.com shanh@google.com -silvajordan@google.com snoeberger@google.com spdonghao@google.com steell@google.com @@ -106,7 +103,6 @@ thiruram@google.com tracyzhou@google.com tsuji@google.com twickham@google.com -uwaisashraf@google.com vadimt@google.com valiiftime@google.com vanjan@google.com @@ -121,3 +117,11 @@ yuandizhou@google.com yurilin@google.com yuzhechen@google.com zakcohen@google.com + +# Overview eng team +alexchau@google.com +samcackett@google.com +silvajordan@google.com +uwaisashraf@google.com +vinayjoglekar@google.com +willosborn@google.com diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 3e241bfe6447..082fb5108f8e 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1977,6 +1977,16 @@ flag { } flag { + name: "hardware_color_styles" + namespace: "systemui" + description: "Enables loading initial colors based ion hardware color" + bug: "347286986" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "shade_launch_accessibility" namespace: "systemui" description: "Intercept accessibility focus events for the Shade during launch animations to avoid stray TalkBack events." diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java index bd811814eb24..4140a956182c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java @@ -18,9 +18,7 @@ package com.android.keyguard; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -28,8 +26,6 @@ import static org.mockito.Mockito.when; import android.hardware.biometrics.BiometricSourceType; import android.testing.TestableLooper; -import android.text.Editable; -import android.text.TextWatcher; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -99,19 +95,6 @@ public class KeyguardMessageAreaControllerTest extends SysuiTestCase { } @Test - public void textChanged_AnnounceForAccessibility() { - ArgumentCaptor<TextWatcher> textWatcherArgumentCaptor = ArgumentCaptor.forClass( - TextWatcher.class); - mMessageAreaController.onViewAttached(); - verify(mKeyguardMessageArea).addTextChangedListener(textWatcherArgumentCaptor.capture()); - - textWatcherArgumentCaptor.getValue().afterTextChanged( - Editable.Factory.getInstance().newEditable("abc")); - verify(mKeyguardMessageArea).removeCallbacks(any(Runnable.class)); - verify(mKeyguardMessageArea).postDelayed(any(Runnable.class), anyLong()); - } - - @Test public void testSetBouncerVisible() { mMessageAreaController.setIsVisible(true); verify(mKeyguardMessageArea).setIsVisible(true); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java new file mode 100644 index 000000000000..ecd04a47b8ae --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.theme; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + + +public class HardwareColorRule implements TestRule { + public String color = ""; + public String[] options = {}; + public boolean isTesting = false; + + @Override + public Statement apply(Statement base, Description description) { + HardwareColors hardwareColors = description.getAnnotation(HardwareColors.class); + if (hardwareColors != null) { + color = hardwareColors.color(); + options = hardwareColors.options(); + isTesting = true; + } + return base; + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java new file mode 100644 index 000000000000..0b8df2e2670e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.theme; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface HardwareColors { + String color(); + String[] options(); +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java index 5cd0846ded7e..9a0b8125fb25 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java @@ -64,6 +64,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.monet.DynamicColors; @@ -77,6 +78,7 @@ import com.android.systemui.util.settings.SecureSettings; import com.google.common.util.concurrent.MoreExecutors; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -98,6 +100,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { private static final UserHandle MANAGED_USER_HANDLE = UserHandle.of(100); private static final UserHandle PRIVATE_USER_HANDLE = UserHandle.of(101); + @Rule + public HardwareColorRule rule = new HardwareColorRule(); + @Mock private JavaAdapter mJavaAdapter; @Mock @@ -148,13 +153,17 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { @Captor private ArgumentCaptor<ContentObserver> mSettingsObserver; + @Mock + private SystemPropertiesHelper mSystemProperties; + @Before public void setup() { MockitoAnnotations.initMocks(this); + when(mFeatureFlags.isEnabled(Flags.MONET)).thenReturn(true); when(mWakefulnessLifecycle.getWakefulness()).thenReturn(WAKEFULNESS_AWAKE); when(mUiModeManager.getContrast()).thenReturn(0.5f); - when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true); + when(mResources.getColor(eq(android.R.color.system_accent1_500), any())) .thenReturn(Color.RED); when(mResources.getColor(eq(android.R.color.system_accent2_500), any())) @@ -166,11 +175,20 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { when(mResources.getColor(eq(android.R.color.system_neutral2_500), any())) .thenReturn(Color.BLACK); + when(mResources.getStringArray(com.android.internal.R.array.theming_defaults)) + .thenReturn(rule.options); + + // should fallback to `*|TONAL_SPOT|home_wallpaper` + when(mSystemProperties.get("ro.boot.hardware.color")).thenReturn(rule.color); + // will try set hardware colors as boot ONLY if user is not set yet + when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(!rule.isTesting); + mThemeOverlayController = new ThemeOverlayController(mContext, mBroadcastDispatcher, mBgHandler, mMainExecutor, mBgExecutor, mThemeOverlayApplier, mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController, mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle, - mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) { + mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager, + mSystemProperties) { @VisibleForTesting protected boolean isNightMode() { return false; @@ -214,11 +232,58 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { public void start_checksWallpaper() { ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); verify(mBgExecutor).execute(registrationRunnable.capture()); + registrationRunnable.getValue().run(); + verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM)); + } + + @Test + @HardwareColors(color = "BLK", options = { + "BLK|MONOCHROMATIC|#FF0000", + "*|VIBRANT|home_wallpaper" + }) + @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES) + public void start_checkHardwareColor() { + // getWallpaperColors should not be called + ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mMainExecutor).execute(registrationRunnable.capture()); + registrationRunnable.getValue().run(); + verify(mWallpaperManager, never()).getWallpaperColors(anyInt()); + + assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.MONOCHROMATIC); + assertThat(mThemeOverlayController.mCurrentColors.get(0).getMainColors().get( + 0).toArgb()).isEqualTo(Color.RED); + } + + @Test + @HardwareColors(color = "", options = { + "BLK|MONOCHROMATIC|#FF0000", + "*|VIBRANT|home_wallpaper" + }) + @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES) + public void start_wildcardColor() { + // getWallpaperColors will be called because we srt wildcard to `home_wallpaper` + ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mMainExecutor).execute(registrationRunnable.capture()); + registrationRunnable.getValue().run(); + verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM)); + assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.VIBRANT); + } + + @Test + @HardwareColors(color = "NONEXISTENT", options = {}) + @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES) + public void start_fallbackColor() { + // getWallpaperColors will be called because we default color source is `home_wallpaper` + ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mMainExecutor).execute(registrationRunnable.capture()); registrationRunnable.getValue().run(); verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM)); + + assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.TONAL_SPOT); } + @Test public void onWallpaperColorsChanged_setsTheme_whenForeground() { // Should ask for a new theme when wallpaper colors change @@ -287,9 +352,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.system_palette\":\"override.package.name\"," - + "\"android.theme.customization.color_source\":\"preset\"}"; + String jsonString = createJsonString(TestColorSource.preset, "override.package.name", + "TONAL_SPOT"); + when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -313,11 +378,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -348,11 +409,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -381,11 +438,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"lock_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.lock_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -404,11 +457,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"lock_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.lock_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -455,8 +504,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { @Test public void onSettingChanged_invalidStyle() { when(mDeviceProvisionedController.isUserSetup(anyInt())).thenReturn(true); - String jsonString = "{\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.theme_style\":\"some_invalid_name\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper, "A16B00", + "some_invalid_name"); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -473,11 +522,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -506,11 +551,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -537,11 +578,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -570,11 +607,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -599,7 +632,6 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { } - @Test @EnableFlags(com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI) public void onWallpaperColorsChanged_homeWallpaperWithSameColor_shouldKeepThemeAndReapply() { @@ -608,11 +640,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(0xffa16b00), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -642,11 +670,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -676,11 +700,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -711,11 +731,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(0xffa16b00), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -745,11 +761,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -886,7 +898,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier, mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController, mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle, - mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) { + mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager, + mSystemProperties) { @VisibleForTesting protected boolean isNightMode() { return false; @@ -926,7 +939,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier, mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController, mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle, - mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) { + mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager, + mSystemProperties) { @VisibleForTesting protected boolean isNightMode() { return false; @@ -992,7 +1006,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { clearInvocations(mThemeOverlayApplier); // Device went to sleep and second set of colors was applied. - mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), + mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), Color.valueOf(Color.RED), null); mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM, USER_SYSTEM); @@ -1018,7 +1032,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { clearInvocations(mThemeOverlayApplier); // Device went to sleep and second set of colors was applied. - mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), + mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), Color.valueOf(Color.RED), null); mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM, USER_SYSTEM); @@ -1034,8 +1048,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.system_palette\":\"00FF00\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper, "00FF00", + "TONAL_SPOT"); + when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -1115,4 +1130,25 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { + DynamicColors.getCustomColorsMapped(false).size() * 2) ).setResourceValue(any(String.class), eq(TYPE_INT_COLOR_ARGB8), anyInt(), eq(null)); } + + private enum TestColorSource { + preset, + home_wallpaper, + lock_wallpaper + } + + private String createJsonString(TestColorSource colorSource, String seedColorHex, + String style) { + return "{\"android.theme.customization.color_source\":\"" + colorSource.toString() + "\"," + + "\"android.theme.customization.system_palette\":\"" + seedColorHex + "\"," + + "\"android.theme.customization.accent_color\":\"" + seedColorHex + "\"," + + "\"android.theme.customization.color_index\":\"2\"," + + "\"android.theme.customization.theme_style\":\"" + style + "\"}"; + } + + private String createJsonString(TestColorSource colorSource) { + return createJsonString(colorSource, "A16B00", "TONAL_SPOT"); + } + + } diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml index 0b35559148af..87d06bfde743 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml @@ -21,7 +21,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/keyguard_lock_padding" - android:importantForAccessibility="no" + android:accessibilityLiveRegion="polite" android:ellipsize="marquee" android:focusable="false" android:gravity="center" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml index f231df2f1a10..c7f320c69113 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml @@ -67,6 +67,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml index 04457229d573..9359838238af 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml @@ -32,6 +32,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml index b184344f2f24..6cbe96a8cb50 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml @@ -68,6 +68,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml index 0e15ff66f3ee..cf388875a174 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml @@ -36,6 +36,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml index f6ac02aee657..33eab179c3f7 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml @@ -75,6 +75,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml index ba4da794d777..fd5eeb8b9408 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml @@ -33,6 +33,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res/layout/notification_2025_hybrid.xml b/packages/SystemUI/res/layout/notification_2025_hybrid.xml index 8fd10fb3ddb8..8c34cd4165e0 100644 --- a/packages/SystemUI/res/layout/notification_2025_hybrid.xml +++ b/packages/SystemUI/res/layout/notification_2025_hybrid.xml @@ -29,7 +29,6 @@ android:layout_height="wrap_content" android:singleLine="true" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@*android:dimen/notification_2025_title_text_size" android:paddingEnd="4dp" /> <TextView diff --git a/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml b/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml index 35f2ef901bdd..a338e4c70cfa 100644 --- a/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml +++ b/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml @@ -54,7 +54,6 @@ android:singleLine="true" android:paddingEnd="4dp" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@*android:dimen/notification_2025_title_text_size" /> <TextView diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java index f528ec8af134..860a496ef18b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java @@ -20,22 +20,16 @@ import android.content.res.ColorStateList; import android.content.res.Configuration; import android.hardware.biometrics.BiometricSourceType; import android.os.SystemClock; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; import android.util.Log; import android.util.Pair; import android.view.View; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.util.ViewController; -import java.lang.ref.WeakReference; - import javax.inject.Inject; /** @@ -54,39 +48,8 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> private Pair<BiometricSourceType, Long> mMessageBiometricSource = null; private static final Long SKIP_SHOWING_FACE_MESSAGE_AFTER_FP_MESSAGE_MS = 3500L; - /** - * Delay before speaking an accessibility announcement. Used to prevent - * lift-to-type from interrupting itself. - */ - private static final long ANNOUNCEMENT_DELAY = 250; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final ConfigurationController mConfigurationController; - private final AnnounceRunnable mAnnounceRunnable; - private final TextWatcher mTextWatcher = new TextWatcher() { - @Override - public void afterTextChanged(Editable editable) { - CharSequence msg = editable; - if (!TextUtils.isEmpty(msg)) { - mView.removeCallbacks(mAnnounceRunnable); - mAnnounceRunnable.setTextToAnnounce(msg); - mView.postDelayed(() -> { - if (msg == mView.getText()) { - mAnnounceRunnable.run(); - } - }, ANNOUNCEMENT_DELAY); - } - } - - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { - /* no-op */ - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { - /* no-op */ - } - }; private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { public void onFinishedGoingToSleep(int why) { @@ -122,7 +85,6 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> mKeyguardUpdateMonitor = keyguardUpdateMonitor; mConfigurationController = configurationController; - mAnnounceRunnable = new AnnounceRunnable(mView); } @Override @@ -131,14 +93,12 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> mKeyguardUpdateMonitor.registerCallback(mInfoCallback); mView.setSelected(mKeyguardUpdateMonitor.isDeviceInteractive()); mView.onThemeChanged(); - mView.addTextChangedListener(mTextWatcher); } @Override protected void onViewDetached() { mConfigurationController.removeCallback(mConfigurationListener); mKeyguardUpdateMonitor.removeCallback(mInfoCallback); - mView.removeTextChangedListener(mTextWatcher); } /** @@ -232,30 +192,4 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> view, mKeyguardUpdateMonitor, mConfigurationController); } } - - /** - * Runnable used to delay accessibility announcements. - */ - @VisibleForTesting - public static class AnnounceRunnable implements Runnable { - private final WeakReference<View> mHost; - private CharSequence mTextToAnnounce; - - AnnounceRunnable(View host) { - mHost = new WeakReference<>(host); - } - - /** Sets the text to announce. */ - public void setTextToAnnounce(CharSequence textToAnnounce) { - mTextToAnnounce = textToAnnounce; - } - - @Override - public void run() { - final View host = mHost.get(); - if (host != null && host.isVisibleToUser()) { - host.announceForAccessibility(mTextToAnnounce); - } - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt index 434a9ce58c3b..7d8945a5b4a7 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt @@ -191,7 +191,6 @@ object KeyguardBouncerViewBinder { .filter { it == EXPANSION_VISIBLE } .collect { securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON) - view.announceForAccessibility(securityContainerController.title) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt index c23ff5302b3c..dc444ffc2a34 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt @@ -16,6 +16,7 @@ package com.android.systemui.shade.shared.flag +import android.window.DesktopExperienceFlags import com.android.systemui.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils @@ -30,10 +31,26 @@ object ShadeWindowGoesAround { val token: FlagToken get() = FlagToken(FLAG_NAME, isEnabled) + /** + * This is defined as [DesktopExperienceFlags] to make it possible to enable it together with + * all the other desktop experience flags from the dev settings. + * + * Alternatively, using adb: + * ```bash + * adb shell aflags enable com.android.window.flags.show_desktop_experience_dev_option && \ + * adb shell setprop persist.wm.debug.desktop_experience_devopts 1 + * ``` + */ + val FLAG = + DesktopExperienceFlags.DesktopExperienceFlag( + Flags::shadeWindowGoesAround, + /* shouldOverrideByDevOption= */ true, + ) + /** Is the refactor enabled */ @JvmStatic inline val isEnabled: Boolean - get() = Flags.shadeWindowGoesAround() + get() = FLAG.isTrue /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 28cf78f6777e..9f60fe212567 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -18,8 +18,10 @@ package com.android.systemui.theme; import static android.util.TypedValue.TYPE_INT_COLOR_ARGB8; +import static com.android.systemui.Flags.hardwareColorStyles; import static com.android.systemui.Flags.themeOverlayControllerWakefulnessDeprecation; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP; +import static com.android.systemui.monet.ColorScheme.GOOGLE_BLUE; import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_HOME; import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_LOCK; import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_PRESET; @@ -73,6 +75,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.KeyguardState; @@ -99,6 +102,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -136,9 +140,11 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { private final DeviceProvisionedController mDeviceProvisionedController; private final Resources mResources; // Current wallpaper colors associated to a user. - private final SparseArray<WallpaperColors> mCurrentColors = new SparseArray<>(); + @VisibleForTesting + protected final SparseArray<WallpaperColors> mCurrentColors = new SparseArray<>(); private final WallpaperManager mWallpaperManager; private final ActivityManager mActivityManager; + protected final SystemPropertiesHelper mSystemPropertiesHelper; @VisibleForTesting protected ColorScheme mColorScheme; // If fabricated overlays were already created for the current theme. @@ -423,7 +429,9 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { JavaAdapter javaAdapter, KeyguardTransitionInteractor keyguardTransitionInteractor, UiModeManager uiModeManager, - ActivityManager activityManager) { + ActivityManager activityManager, + SystemPropertiesHelper systemPropertiesHelper + ) { mContext = context; mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET); mIsFidelityEnabled = featureFlags.isEnabled(Flags.COLOR_FIDELITY); @@ -443,6 +451,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { mKeyguardTransitionInteractor = keyguardTransitionInteractor; mUiModeManager = uiModeManager; mActivityManager = activityManager; + mSystemPropertiesHelper = systemPropertiesHelper; dumpManager.registerDumpable(TAG, this); Flow<Boolean> isFinishedInAsleepStateFlow = mKeyguardTransitionInteractor @@ -498,29 +507,38 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor); mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); + WallpaperColors systemColor; + if (hardwareColorStyles() && !mDeviceProvisionedController.isCurrentUserSetup()) { + Pair<Integer, Color> defaultSettings = getThemeSettingsDefaults(); + mThemeStyle = defaultSettings.first; + Color seedColor = defaultSettings.second; + + // we only use the first color anyway, so we can pass only the single color we have + systemColor = new WallpaperColors( + /*primaryColor*/ seedColor, + /*secondaryColor*/ seedColor, + /*tertiaryColor*/ seedColor + ); + } else { + systemColor = mWallpaperManager.getWallpaperColors( + getDefaultWallpaperColorsSource(mUserTracker.getUserId())); + } + // Upon boot, make sure we have the most up to date colors Runnable updateColors = () -> { - WallpaperColors systemColor = mWallpaperManager.getWallpaperColors( - getDefaultWallpaperColorsSource(mUserTracker.getUserId())); - Runnable applyColors = () -> { - if (DEBUG) Log.d(TAG, "Boot colors: " + systemColor); - mCurrentColors.put(mUserTracker.getUserId(), systemColor); - reevaluateSystemTheme(false /* forceReload */); - }; - if (mDeviceProvisionedController.isCurrentUserSetup()) { - mMainExecutor.execute(applyColors); - } else { - applyColors.run(); - } + if (DEBUG) Log.d(TAG, "Boot colors: " + systemColor); + mCurrentColors.put(mUserTracker.getUserId(), systemColor); + reevaluateSystemTheme(false /* forceReload */); }; // Whenever we're going directly to setup wizard, we need to process colors synchronously, // otherwise we'll see some jank when the activity is recreated. if (!mDeviceProvisionedController.isCurrentUserSetup()) { - updateColors.run(); + mMainExecutor.execute(updateColors); } else { mBgExecutor.execute(updateColors); } + mWallpaperManager.addOnColorsChangedListener(mOnColorsChangedListener, null, UserHandle.USER_ALL); @@ -604,7 +622,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { @VisibleForTesting protected boolean isPrivateProfile(UserHandle userHandle) { - Context usercontext = mContext.createContextAsUser(userHandle,0); + Context usercontext = mContext.createContextAsUser(userHandle, 0); return usercontext.getSystemService(UserManager.class).isPrivateProfile(); } @@ -720,6 +738,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { return true; } + @SuppressWarnings("StringCaseLocaleUsage") // Package name is not localized private void updateThemeOverlays() { final int currentUser = mUserTracker.getUserId(); final String overlayPackageJson = mSecureSettings.getStringForUser( @@ -746,7 +765,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { OverlayIdentifier systemPalette = categoryToPackage.get(OVERLAY_CATEGORY_SYSTEM_PALETTE); if (mIsMonetEnabled && systemPalette != null && systemPalette.getPackageName() != null) { try { - String colorString = systemPalette.getPackageName().toLowerCase(); + String colorString = systemPalette.getPackageName().toLowerCase(); if (!colorString.startsWith("#")) { colorString = "#" + colorString; } @@ -856,6 +875,75 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { return style; } + protected Pair<Integer, String> getHardwareColorSetting() { + String deviceColorProperty = "ro.boot.hardware.color"; + + String[] themeData = mResources.getStringArray( + com.android.internal.R.array.theming_defaults); + + // Color can be hex (`#FF0000`) or `home_wallpaper` + Map<String, Pair<Integer, String>> themeMap = new HashMap<>(); + + // extract all theme settings + for (String themeEntry : themeData) { + String[] themeComponents = themeEntry.split("\\|"); + if (themeComponents.length != 3) continue; + themeMap.put(themeComponents[0], + new Pair<>(Style.valueOf(themeComponents[1]), themeComponents[2])); + } + + Pair<Integer, String> fallbackTheme = themeMap.get("*"); + if (fallbackTheme == null) { + Log.d(TAG, "Theming wildcard not found. Fallback to TONAL_SPOT|" + COLOR_SOURCE_HOME); + fallbackTheme = new Pair<>(Style.TONAL_SPOT, COLOR_SOURCE_HOME); + } + + String deviceColorPropertyValue = mSystemPropertiesHelper.get(deviceColorProperty); + Pair<Integer, String> selectedTheme = themeMap.get(deviceColorPropertyValue); + if (selectedTheme == null) { + Log.d(TAG, "Sysprop `" + deviceColorProperty + "` of value '" + deviceColorPropertyValue + + "' not found in theming_defaults: " + Arrays.toString(themeData)); + selectedTheme = fallbackTheme; + } + + return selectedTheme; + } + + @VisibleForTesting + protected Pair<Integer, Color> getThemeSettingsDefaults() { + + Pair<Integer, String> selectedTheme = getHardwareColorSetting(); + + // Last fallback color + Color defaultSeedColor = Color.valueOf(GOOGLE_BLUE); + + // defaultColor will come from wallpaper or be parsed from a string + boolean isWallpaper = selectedTheme.second.equals(COLOR_SOURCE_HOME); + + if (isWallpaper) { + WallpaperColors wallpaperColors = mWallpaperManager.getWallpaperColors( + getDefaultWallpaperColorsSource(mUserTracker.getUserId())); + + if (wallpaperColors != null) { + defaultSeedColor = wallpaperColors.getPrimaryColor(); + } + + Log.d(TAG, "Default seed color read from home wallpaper: " + Integer.toHexString( + defaultSeedColor.toArgb())); + } else { + try { + defaultSeedColor = Color.valueOf(Color.parseColor(selectedTheme.second)); + Log.d(TAG, "Default seed color read from resource: " + Integer.toHexString( + defaultSeedColor.toArgb())); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Error parsing color: " + selectedTheme.second, e); + // defaultSeedColor remains unchanged in this case + } + } + + return new Pair<>(selectedTheme.first, defaultSeedColor); + } + @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("mSystemColors=" + mCurrentColors); diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 6d6e1fb6bfb3..183990c01072 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -18,6 +18,7 @@ package com.android.server.audio; import static android.media.audio.Flags.scoManagedByAudio; import static com.android.media.audio.Flags.equalScoLeaVcIndexRange; +import static com.android.media.audio.Flags.optimizeBtDeviceSwitch; import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_BLE_HEADSET; import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_BLE_SPEAKER; import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_SCO; @@ -290,8 +291,8 @@ public class AudioDeviceBroker { } @GuardedBy("mDeviceStateLock") - /*package*/ void onSetBtScoActiveDevice(BluetoothDevice btDevice) { - mBtHelper.onSetBtScoActiveDevice(btDevice); + /*package*/ void onSetBtScoActiveDevice(BluetoothDevice btDevice, boolean deviceSwitch) { + mBtHelper.onSetBtScoActiveDevice(btDevice, deviceSwitch); } /*package*/ void setBluetoothA2dpOn_Async(boolean on, String source) { @@ -941,6 +942,7 @@ public class AudioDeviceBroker { final @NonNull String mEventSource; final int mAudioSystemDevice; final int mMusicDevice; + final boolean mIsDeviceSwitch; BtDeviceInfo(@NonNull BtDeviceChangedData d, @NonNull BluetoothDevice device, int state, int audioDevice, @AudioSystem.AudioFormatNativeEnumForBtCodec int codec) { @@ -953,6 +955,8 @@ public class AudioDeviceBroker { mEventSource = d.mEventSource; mAudioSystemDevice = audioDevice; mMusicDevice = AudioSystem.DEVICE_NONE; + mIsDeviceSwitch = optimizeBtDeviceSwitch() + && d.mNewDevice != null && d.mPreviousDevice != null; } // constructor used by AudioDeviceBroker to search similar message @@ -966,6 +970,7 @@ public class AudioDeviceBroker { mSupprNoisy = false; mVolume = -1; mIsLeOutput = false; + mIsDeviceSwitch = false; } // constructor used by AudioDeviceInventory when config change failed @@ -980,6 +985,7 @@ public class AudioDeviceBroker { mSupprNoisy = false; mVolume = -1; mIsLeOutput = false; + mIsDeviceSwitch = false; } BtDeviceInfo(@NonNull BtDeviceInfo src, int state) { @@ -992,6 +998,7 @@ public class AudioDeviceBroker { mEventSource = src.mEventSource; mAudioSystemDevice = src.mAudioSystemDevice; mMusicDevice = src.mMusicDevice; + mIsDeviceSwitch = false; } // redefine equality op so we can match messages intended for this device @@ -1026,7 +1033,8 @@ public class AudioDeviceBroker { + " isLeOutput=" + mIsLeOutput + " eventSource=" + mEventSource + " audioSystemDevice=" + mAudioSystemDevice - + " musicDevice=" + mMusicDevice; + + " musicDevice=" + mMusicDevice + + " isDeviceSwitch=" + mIsDeviceSwitch; } } @@ -1680,10 +1688,11 @@ public class AudioDeviceBroker { } /*package*/ boolean handleDeviceConnection(@NonNull AudioDeviceAttributes attributes, - boolean connect, @Nullable BluetoothDevice btDevice) { + boolean connect, @Nullable BluetoothDevice btDevice, + boolean deviceSwitch) { synchronized (mDeviceStateLock) { return mDeviceInventory.handleDeviceConnection( - attributes, connect, false /*for test*/, btDevice); + attributes, connect, false /*for test*/, btDevice, deviceSwitch); } } @@ -1930,10 +1939,12 @@ public class AudioDeviceBroker { || btInfo.mIsLeOutput) ? mAudioService.getBluetoothContextualVolumeStream() : AudioSystem.STREAM_DEFAULT); - if (btInfo.mProfile == BluetoothProfile.LE_AUDIO + if ((btInfo.mProfile == BluetoothProfile.LE_AUDIO || btInfo.mProfile == BluetoothProfile.HEARING_AID || (mScoManagedByAudio - && btInfo.mProfile == BluetoothProfile.HEADSET)) { + && btInfo.mProfile == BluetoothProfile.HEADSET)) + && (btInfo.mState == BluetoothProfile.STATE_CONNECTED + || !btInfo.mIsDeviceSwitch)) { onUpdateCommunicationRouteClient( bluetoothScoRequestOwnerAttributionSource(), "setBluetoothActiveDevice"); diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index ef10793fd955..ae91934e7498 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -799,7 +799,7 @@ public class AudioDeviceInventory { di.mDeviceAddress, di.mDeviceName), AudioSystem.DEVICE_STATE_AVAILABLE, - di.mDeviceCodecFormat); + di.mDeviceCodecFormat, false /*deviceSwitch*/); if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) { failedReconnectionDeviceList.add(di); } @@ -811,7 +811,7 @@ public class AudioDeviceInventory { EventLogger.Event.ALOGE, TAG); mConnectedDevices.remove(di.getKey(), di); if (AudioSystem.isBluetoothScoDevice(di.mDeviceType)) { - mDeviceBroker.onSetBtScoActiveDevice(null); + mDeviceBroker.onSetBtScoActiveDevice(null, false /*deviceSwitch*/); } } } @@ -851,7 +851,8 @@ public class AudioDeviceInventory { Log.d(TAG, "onSetBtActiveDevice" + " btDevice=" + btInfo.mDevice + " profile=" + BluetoothProfile.getProfileName(btInfo.mProfile) - + " state=" + BluetoothProfile.getConnectionStateName(btInfo.mState)); + + " state=" + BluetoothProfile.getConnectionStateName(btInfo.mState) + + " isDeviceSwitch=" + btInfo.mIsDeviceSwitch); } String address = btInfo.mDevice.getAddress(); if (!BluetoothAdapter.checkBluetoothAddress(address)) { @@ -897,7 +898,8 @@ public class AudioDeviceInventory { break; case BluetoothProfile.A2DP: if (switchToUnavailable) { - makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat); + makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat, + btInfo.mIsDeviceSwitch); } else if (switchToAvailable) { // device is not already connected if (btInfo.mVolume != -1) { @@ -911,7 +913,7 @@ public class AudioDeviceInventory { break; case BluetoothProfile.HEARING_AID: if (switchToUnavailable) { - makeHearingAidDeviceUnavailable(address); + makeHearingAidDeviceUnavailable(address, btInfo.mIsDeviceSwitch); } else if (switchToAvailable) { makeHearingAidDeviceAvailable(address, BtHelper.getName(btInfo.mDevice), streamType, "onSetBtActiveDevice"); @@ -921,7 +923,8 @@ public class AudioDeviceInventory { case BluetoothProfile.LE_AUDIO_BROADCAST: if (switchToUnavailable) { makeLeAudioDeviceUnavailableNow(address, - btInfo.mAudioSystemDevice, di.mDeviceCodecFormat); + btInfo.mAudioSystemDevice, di.mDeviceCodecFormat, + btInfo.mIsDeviceSwitch); } else if (switchToAvailable) { makeLeAudioDeviceAvailable( btInfo, streamType, codec, "onSetBtActiveDevice"); @@ -930,9 +933,10 @@ public class AudioDeviceInventory { case BluetoothProfile.HEADSET: if (mDeviceBroker.isScoManagedByAudio()) { if (switchToUnavailable) { - mDeviceBroker.onSetBtScoActiveDevice(null); + mDeviceBroker.onSetBtScoActiveDevice(null, btInfo.mIsDeviceSwitch); } else if (switchToAvailable) { - mDeviceBroker.onSetBtScoActiveDevice(btInfo.mDevice); + mDeviceBroker.onSetBtScoActiveDevice( + btInfo.mDevice, false /*deviceSwitch*/); } } break; @@ -1053,19 +1057,19 @@ public class AudioDeviceInventory { /*package*/ void onMakeA2dpDeviceUnavailableNow(String address, int a2dpCodec) { synchronized (mDevicesLock) { - makeA2dpDeviceUnavailableNow(address, a2dpCodec); + makeA2dpDeviceUnavailableNow(address, a2dpCodec, false /*deviceSwitch*/); } } /*package*/ void onMakeLeAudioDeviceUnavailableNow(String address, int device, int codec) { synchronized (mDevicesLock) { - makeLeAudioDeviceUnavailableNow(address, device, codec); + makeLeAudioDeviceUnavailableNow(address, device, codec, false /*deviceSwitch*/); } } /*package*/ void onMakeHearingAidDeviceUnavailableNow(String address) { synchronized (mDevicesLock) { - makeHearingAidDeviceUnavailable(address); + makeHearingAidDeviceUnavailable(address, false /*deviceSwitch*/); } } @@ -1180,7 +1184,8 @@ public class AudioDeviceInventory { } if (!handleDeviceConnection(wdcs.mAttributes, - wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest, null)) { + wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest, + null, false /*deviceSwitch*/)) { // change of connection state failed, bailout mmi.set(MediaMetrics.Property.EARLY_RETURN, "change of connection state failed") .record(); @@ -1788,14 +1793,15 @@ public class AudioDeviceInventory { */ /*package*/ boolean handleDeviceConnection(@NonNull AudioDeviceAttributes attributes, boolean connect, boolean isForTesting, - @Nullable BluetoothDevice btDevice) { + @Nullable BluetoothDevice btDevice, + boolean deviceSwitch) { int device = attributes.getInternalType(); String address = attributes.getAddress(); String deviceName = attributes.getName(); if (AudioService.DEBUG_DEVICES) { Slog.i(TAG, "handleDeviceConnection(" + connect + " dev:" + Integer.toHexString(device) + " address:" + address - + " name:" + deviceName + ")"); + + " name:" + deviceName + ", deviceSwitch: " + deviceSwitch + ")"); } MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "handleDeviceConnection") .set(MediaMetrics.Property.ADDRESS, address) @@ -1829,7 +1835,8 @@ public class AudioDeviceInventory { res = AudioSystem.AUDIO_STATUS_OK; } else { res = mAudioSystem.setDeviceConnectionState(attributes, - AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT, + false /*deviceSwitch*/); } if (res != AudioSystem.AUDIO_STATUS_OK) { final String reason = "not connecting device 0x" + Integer.toHexString(device) @@ -1856,7 +1863,8 @@ public class AudioDeviceInventory { status = true; } else if (!connect && isConnected) { mAudioSystem.setDeviceConnectionState(attributes, - AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT, + deviceSwitch); // always remove even if disconnection failed mConnectedDevices.remove(deviceKey); mDeviceBroker.postCheckCommunicationDeviceRemoval(attributes); @@ -2030,7 +2038,7 @@ public class AudioDeviceInventory { } } if (disconnect) { - mDeviceBroker.onSetBtScoActiveDevice(null); + mDeviceBroker.onSetBtScoActiveDevice(null, false /*deviceSwitch*/); } } @@ -2068,7 +2076,8 @@ public class AudioDeviceInventory { || info.mProfile == BluetoothProfile.LE_AUDIO_BROADCAST) && info.mIsLeOutput) || info.mProfile == BluetoothProfile.HEARING_AID - || info.mProfile == BluetoothProfile.A2DP)) { + || info.mProfile == BluetoothProfile.A2DP) + && !info.mIsDeviceSwitch) { @AudioService.ConnectionState int asState = (info.mState == BluetoothProfile.STATE_CONNECTED) ? AudioService.CONNECTION_STATE_CONNECTED @@ -2124,7 +2133,7 @@ public class AudioDeviceInventory { AudioDeviceAttributes ada = new AudioDeviceAttributes( AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, name); final int res = mAudioSystem.setDeviceConnectionState(ada, - AudioSystem.DEVICE_STATE_AVAILABLE, codec); + AudioSystem.DEVICE_STATE_AVAILABLE, codec, false); // TODO: log in MediaMetrics once distinction between connection failure and // double connection is made. @@ -2362,7 +2371,7 @@ public class AudioDeviceInventory { } @GuardedBy("mDevicesLock") - private void makeA2dpDeviceUnavailableNow(String address, int codec) { + private void makeA2dpDeviceUnavailableNow(String address, int codec, boolean deviceSwitch) { MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "a2dp." + address) .set(MediaMetrics.Property.ENCODING, AudioSystem.audioFormatToString(codec)) .set(MediaMetrics.Property.EVENT, "makeA2dpDeviceUnavailableNow"); @@ -2393,7 +2402,7 @@ public class AudioDeviceInventory { AudioDeviceAttributes ada = new AudioDeviceAttributes( AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address); final int res = mAudioSystem.setDeviceConnectionState(ada, - AudioSystem.DEVICE_STATE_UNAVAILABLE, codec); + AudioSystem.DEVICE_STATE_UNAVAILABLE, codec, deviceSwitch); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( @@ -2404,7 +2413,8 @@ public class AudioDeviceInventory { } else { AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address) - + " made unavailable")).printSlog(EventLogger.Event.ALOGI, TAG)); + + " made unavailable, deviceSwitch" + deviceSwitch)) + .printSlog(EventLogger.Event.ALOGI, TAG)); } mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP); @@ -2440,7 +2450,7 @@ public class AudioDeviceInventory { final int res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes( AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address), AudioSystem.DEVICE_STATE_AVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.AUDIO_FORMAT_DEFAULT, false); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "APM failed to make available A2DP source device addr=" @@ -2465,7 +2475,7 @@ public class AudioDeviceInventory { AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address); mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.AUDIO_FORMAT_DEFAULT, false); // always remove regardless of the result mConnectedDevices.remove( DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address)); @@ -2485,7 +2495,7 @@ public class AudioDeviceInventory { DEVICE_OUT_HEARING_AID, address, name); final int res = mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.AUDIO_FORMAT_DEFAULT, false); if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueueAndSlog( "APM failed to make available HearingAid addr=" + address @@ -2515,12 +2525,12 @@ public class AudioDeviceInventory { } @GuardedBy("mDevicesLock") - private void makeHearingAidDeviceUnavailable(String address) { + private void makeHearingAidDeviceUnavailable(String address, boolean deviceSwitch) { AudioDeviceAttributes ada = new AudioDeviceAttributes( DEVICE_OUT_HEARING_AID, address); mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.AUDIO_FORMAT_DEFAULT, deviceSwitch); // always remove regardless of return code mConnectedDevices.remove( DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address)); @@ -2622,7 +2632,7 @@ public class AudioDeviceInventory { AudioDeviceAttributes ada = new AudioDeviceAttributes(device, address, name); final int res = mAudioSystem.setDeviceConnectionState(ada, - AudioSystem.DEVICE_STATE_AVAILABLE, codec); + AudioSystem.DEVICE_STATE_AVAILABLE, codec, false /*deviceSwitch*/); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueueAndSlog( "APM failed to make available LE Audio device addr=" + address @@ -2669,13 +2679,13 @@ public class AudioDeviceInventory { @GuardedBy("mDevicesLock") private void makeLeAudioDeviceUnavailableNow(String address, int device, - @AudioSystem.AudioFormatNativeEnumForBtCodec int codec) { + @AudioSystem.AudioFormatNativeEnumForBtCodec int codec, boolean deviceSwitch) { AudioDeviceAttributes ada = null; if (device != AudioSystem.DEVICE_NONE) { ada = new AudioDeviceAttributes(device, address); final int res = mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, - codec); + codec, deviceSwitch); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( @@ -2685,7 +2695,8 @@ public class AudioDeviceInventory { } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address) - + " made unavailable").printSlog(EventLogger.Event.ALOGI, TAG)); + + " made unavailable, deviceSwitch" + deviceSwitch) + .printSlog(EventLogger.Event.ALOGI, TAG)); } mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address)); } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 86871ea45d13..057d1274d47d 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -63,6 +63,7 @@ import static com.android.media.audio.Flags.audioserverPermissions; import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume; import static com.android.media.audio.Flags.deferWearPermissionUpdates; import static com.android.media.audio.Flags.equalScoLeaVcIndexRange; +import static com.android.media.audio.Flags.optimizeBtDeviceSwitch; import static com.android.media.audio.Flags.replaceStreamBtSco; import static com.android.media.audio.Flags.ringMyCar; import static com.android.media.audio.Flags.ringerModeAffectsAlarm; @@ -4990,6 +4991,8 @@ public class AudioService extends IAudioService.Stub + cacheGetStreamMinMaxVolume()); pw.println("\tandroid.media.audio.Flags.cacheGetStreamVolume:" + cacheGetStreamVolume()); + pw.println("\tcom.android.media.audio.optimizeBtDeviceSwitch:" + + optimizeBtDeviceSwitch()); } private void dumpAudioMode(PrintWriter pw) { diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index e86c34cab88a..a6267c156fb3 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -367,9 +367,9 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, * @return */ public int setDeviceConnectionState(AudioDeviceAttributes attributes, int state, - int codecFormat) { + int codecFormat, boolean deviceSwitch) { invalidateRoutingCache(); - return AudioSystem.setDeviceConnectionState(attributes, state, codecFormat); + return AudioSystem.setDeviceConnectionState(attributes, state, codecFormat, deviceSwitch); } /** diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 922116999bc7..844e3524384d 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -26,6 +26,8 @@ import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER; import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN; import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_WATCH; +import static com.android.media.audio.Flags.optimizeBtDeviceSwitch; + import android.annotation.NonNull; import android.annotation.Nullable; import android.bluetooth.BluetoothA2dp; @@ -393,8 +395,11 @@ public class BtHelper { + "received with null profile proxy for device: " + btDevice)).printLog(TAG)); return; + } - onSetBtScoActiveDevice(btDevice); + boolean deviceSwitch = optimizeBtDeviceSwitch() + && btDevice != null && mBluetoothHeadsetDevice != null; + onSetBtScoActiveDevice(btDevice, deviceSwitch); } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { int btState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); onScoAudioStateChanged(btState); @@ -814,7 +819,7 @@ public class BtHelper { if (device == null) { continue; } - onSetBtScoActiveDevice(device); + onSetBtScoActiveDevice(device, false /*deviceSwitch*/); } } else { Log.e(TAG, "onHeadsetProfileConnected: Null BluetoothAdapter"); @@ -907,7 +912,8 @@ public class BtHelper { } @GuardedBy("mDeviceBroker.mDeviceStateLock") - private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) { + private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive, + boolean deviceSwitch) { if (btDevice == null) { return true; } @@ -919,12 +925,12 @@ public class BtHelper { if (isActive) { audioDevice = btHeadsetDeviceToAudioDevice(btDevice); result = mDeviceBroker.handleDeviceConnection( - audioDevice, true /*connect*/, btDevice); + audioDevice, true /*connect*/, btDevice, false /*deviceSwitch*/); } else { AudioDeviceAttributes ada = mResolvedScoAudioDevices.get(btDevice); if (ada != null) { result = mDeviceBroker.handleDeviceConnection( - ada, false /*connect*/, btDevice); + ada, false /*connect*/, btDevice, deviceSwitch); } else { // Disconnect all possible audio device types if the disconnected device type is // unknown @@ -935,7 +941,8 @@ public class BtHelper { }; for (int outDeviceType : outDeviceTypes) { result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( - outDeviceType, address, name), false /*connect*/, btDevice); + outDeviceType, address, name), false /*connect*/, btDevice, + deviceSwitch); } } } @@ -944,7 +951,7 @@ public class BtHelper { // handleDeviceConnection() && result to make sure the method get executed result = mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( inDevice, address, name), - isActive, btDevice) && result; + isActive, btDevice, deviceSwitch) && result; if (result) { if (isActive) { mResolvedScoAudioDevices.put(btDevice, audioDevice); @@ -961,18 +968,18 @@ public class BtHelper { } @GuardedBy("mDeviceBroker.mDeviceStateLock") - /*package */ void onSetBtScoActiveDevice(BluetoothDevice btDevice) { + /*package */ void onSetBtScoActiveDevice(BluetoothDevice btDevice, boolean deviceSwitch) { Log.i(TAG, "onSetBtScoActiveDevice: " + getAnonymizedAddress(mBluetoothHeadsetDevice) - + " -> " + getAnonymizedAddress(btDevice)); + + " -> " + getAnonymizedAddress(btDevice) + ", deviceSwitch: " + deviceSwitch); final BluetoothDevice previousActiveDevice = mBluetoothHeadsetDevice; if (Objects.equals(btDevice, previousActiveDevice)) { return; } - if (!handleBtScoActiveDeviceChange(previousActiveDevice, false)) { + if (!handleBtScoActiveDeviceChange(previousActiveDevice, false, deviceSwitch)) { Log.w(TAG, "onSetBtScoActiveDevice() failed to remove previous device " + getAnonymizedAddress(previousActiveDevice)); } - if (!handleBtScoActiveDeviceChange(btDevice, true)) { + if (!handleBtScoActiveDeviceChange(btDevice, true, false /*deviceSwitch*/)) { Log.e(TAG, "onSetBtScoActiveDevice() failed to add new device " + getAnonymizedAddress(btDevice)); // set mBluetoothHeadsetDevice to null when failing to add new device diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index 70143f1c1a98..acdc0e0cf891 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -456,9 +456,8 @@ flag { flag { name: "enable_display_content_mode_management" namespace: "lse_desktop_experience" - description: "Enable switching the content mode of connected displays between mirroring and extened. Also change the default content mode to extended mode." + description: "Enable switching the content mode of connected displays between mirroring and extended. Also change the default content mode to extended mode." bug: "378385869" - is_fixed_read_only: true } flag { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 334e7b5240ce..70044bd813ce 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1332,8 +1332,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // Do not reset the default (current) IME when it is a 3rd-party IME String selectedMethodId = bindingController.getSelectedMethodId(); final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - if (selectedMethodId != null && settings.getMethodMap().get(selectedMethodId) != null - && !settings.getMethodMap().get(selectedMethodId).isSystem()) { + final InputMethodInfo selectedImi = settings.getMethodMap().get(selectedMethodId); + if (selectedImi != null && !selectedImi.isSystem()) { return; } final List<InputMethodInfo> suitableImes = InputMethodInfoUtils.getDefaultEnabledImes( diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java index a96c477c78d2..f731b50d81b4 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionService.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java @@ -17,6 +17,8 @@ package com.android.server.supervision; import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import static android.Manifest.permission.MANAGE_USERS; +import static android.Manifest.permission.QUERY_USERS; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static com.android.internal.util.Preconditions.checkCallAuthorization; @@ -79,6 +81,25 @@ public class SupervisionService extends ISupervisionManager.Stub { } /** + * Creates an {@link Intent} that can be used with {@link Context#startActivity(Intent)} to + * launch the activity to verify supervision credentials. + * + * <p>A valid {@link Intent} is always returned if supervision is enabled at the time this + * method is called, the launched activity still need to perform validity checks as the + * supervision state can change when it's launched. A null intent is returned if supervision is + * disabled at the time of this method call. + * + * <p>A result code of {@link android.app.Activity#RESULT_OK} indicates successful verification + * of the supervision credentials. + */ + @Override + @Nullable + public Intent createConfirmSupervisionCredentialsIntent() { + // TODO(b/392961554): Implement createAuthenticationIntent API + throw new UnsupportedOperationException(); + } + + /** * Returns whether supervision is enabled for the given user. * * <p>Supervision is automatically enabled when the supervision app becomes the profile owner or @@ -86,6 +107,7 @@ public class SupervisionService extends ISupervisionManager.Stub { */ @Override public boolean isSupervisionEnabledForUser(@UserIdInt int userId) { + enforceAnyPermission(QUERY_USERS, MANAGE_USERS); if (UserHandle.getUserId(Binder.getCallingUid()) != userId) { enforcePermission(INTERACT_ACROSS_USERS); } @@ -96,6 +118,7 @@ public class SupervisionService extends ISupervisionManager.Stub { @Override public void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) { + // TODO(b/395630828): Ensure that this method can only be called by the system. if (UserHandle.getUserId(Binder.getCallingUid()) != userId) { enforcePermission(INTERACT_ACROSS_USERS); } @@ -181,8 +204,8 @@ public class SupervisionService extends ISupervisionManager.Stub { * Ensures that supervision is enabled when the supervision app is the profile owner. * * <p>The state syncing with the DevicePolicyManager can only enable supervision and never - * disable. Supervision can only be disabled explicitly via calls to the - * {@link #setSupervisionEnabledForUser} method. + * disable. Supervision can only be disabled explicitly via calls to the {@link + * #setSupervisionEnabledForUser} method. */ private void syncStateWithDevicePolicyManager(@UserIdInt int userId) { final DevicePolicyManagerInternal dpmInternal = mInjector.getDpmInternal(); @@ -221,6 +244,17 @@ public class SupervisionService extends ISupervisionManager.Stub { mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED); } + /** Enforces that the caller has at least one of the given permission. */ + private void enforceAnyPermission(String... permissions) { + boolean authorized = false; + for (String permission : permissions) { + if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) { + authorized = true; + } + } + checkCallAuthorization(authorized); + } + /** Provides local services in a lazy manner. */ static class Injector { private final Context mContext; @@ -280,7 +314,7 @@ public class SupervisionService extends ISupervisionManager.Stub { } @VisibleForTesting - @SuppressLint("MissingPermission") // not needed for a system service + @SuppressLint("MissingPermission") void registerProfileOwnerListener() { IntentFilter poIntentFilter = new IntentFilter(); poIntentFilter.addAction(DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java index 5d64cb638702..2d3f7231cc5c 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java @@ -34,12 +34,12 @@ import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; +import android.app.ActivityManager; import android.app.Instrumentation; import android.content.res.Configuration; import android.graphics.Insets; +import android.os.Build; import android.os.RemoteException; -import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; import android.server.wm.WindowManagerStateHelper; @@ -86,25 +86,36 @@ public class InputMethodServiceTest { "android:id/input_method_nav_back"; private static final String INPUT_METHOD_NAV_IME_SWITCHER_ID = "android:id/input_method_nav_ime_switcher"; - private static final long TIMEOUT_IN_SECONDS = 3; - private static final String ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD = - "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 1"; - private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD = - "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0"; + + /** Timeout until the uiObject should be found. */ + private static final long TIMEOUT_MS = 5000L * Build.HW_TIMEOUT_MULTIPLIER; + + /** Timeout until the event is expected. */ + private static final long EXPECT_TIMEOUT_MS = 3000L * Build.HW_TIMEOUT_MULTIPLIER; + + /** Timeout during which the event is not expected. */ + private static final long NOT_EXCEPT_TIMEOUT_MS = 2000L * Build.HW_TIMEOUT_MULTIPLIER; + + /** Command to set showing the IME when a hardware keyboard is connected. */ + private static final String SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD = + "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD; + /** Command to get verbose ImeTracker logging state. */ + private static final String GET_VERBOSE_IME_TRACKER_LOGGING_CMD = + "getprop persist.debug.imetracker"; + /** Command to set verbose ImeTracker logging state. */ + private static final String SET_VERBOSE_IME_TRACKER_LOGGING_CMD = + "setprop persist.debug.imetracker"; /** The ids of the subtypes of SimpleIme. */ private static final int[] SUBTYPE_IDS = new int[]{1, 2}; - private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper(); + private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper(); private final GestureNavSwitchHelper mGestureNavSwitchHelper = new GestureNavSwitchHelper(); private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider(); @Rule - public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule(mFlagsValueProvider); - - @Rule public final TestName mName = new TestName(); private Instrumentation mInstrumentation; @@ -114,7 +125,8 @@ public class InputMethodServiceTest { private String mInputMethodId; private TestActivity mActivity; private InputMethodServiceWrapper mInputMethodService; - private boolean mShowImeWithHardKeyboardEnabled; + private boolean mOriginalVerboseImeTrackerLoggingEnabled; + private boolean mOriginalShowImeWithHardKeyboardEnabled; @Before public void setUp() throws Exception { @@ -123,9 +135,12 @@ public class InputMethodServiceTest { mImm = mInstrumentation.getContext().getSystemService(InputMethodManager.class); mTargetPackageName = mInstrumentation.getTargetContext().getPackageName(); mInputMethodId = getInputMethodId(); + mOriginalVerboseImeTrackerLoggingEnabled = getVerboseImeTrackerLogging(); + if (!mOriginalVerboseImeTrackerLoggingEnabled) { + setVerboseImeTrackerLogging(true); + } prepareIme(); prepareActivity(); - mInstrumentation.waitForIdleSync(); mUiDevice.freezeRotation(); mUiDevice.setOrientationNatural(); // Waits for input binding ready. @@ -148,17 +163,18 @@ public class InputMethodServiceTest { .that(mInputMethodService.getCurrentInputViewStarted()).isFalse(); }); // Save the original value of show_ime_with_hard_keyboard from Settings. - mShowImeWithHardKeyboardEnabled = + mOriginalShowImeWithHardKeyboardEnabled = mInputMethodService.getShouldShowImeWithHardKeyboardForTesting(); } @After public void tearDown() throws Exception { mUiDevice.unfreezeRotation(); + if (!mOriginalVerboseImeTrackerLoggingEnabled) { + setVerboseImeTrackerLogging(false); + } // Change back the original value of show_ime_with_hard_keyboard in Settings. - executeShellCommand(mShowImeWithHardKeyboardEnabled - ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD - : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD); + setShowImeWithHardKeyboard(mOriginalShowImeWithHardKeyboardEnabled); executeShellCommand("ime disable " + mInputMethodId); } @@ -170,7 +186,7 @@ public class InputMethodServiceTest { public void testShowHideKeyboard_byUserAction() { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); // Performs click on EditText to bring up the IME. Log.i(TAG, "Click on EditText"); @@ -201,14 +217,12 @@ public class InputMethodServiceTest { */ @Test public void testShowHideKeyboard_byInputMethodManager() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // Triggers to show IME via public API. verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - // Triggers to hide IME via public API. verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.hideImeWithInputMethodManager(0 /* flags */)).isTrue(), EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); @@ -219,14 +233,12 @@ public class InputMethodServiceTest { */ @Test public void testShowHideKeyboard_byInsetsController() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // Triggers to show IME via public API. verifyInputViewStatusOnMainSync( () -> mActivity.showImeWithWindowInsetsController(), EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - // Triggers to hide IME via public API. verifyInputViewStatusOnMainSync( () -> mActivity.hideImeWithWindowInsetsController(), EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); @@ -234,53 +246,18 @@ public class InputMethodServiceTest { /** * This checks the result of calling IMS#requestShowSelf and IMS#requestHideSelf. - * - * <p>With the refactor in b/298172246, all calls to IMMS#{show,hide}MySoftInputLocked - * will be just apply the requested visibility (by using the callback). Therefore, we will - * lose flags like HIDE_IMPLICIT_ONLY. */ @Test public void testShowHideSelf() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // IME request to show itself without any flags, expect shown. - Log.i(TAG, "Call IMS#requestShowSelf(0)"); verifyInputViewStatusOnMainSync( () -> mInputMethodService.requestShowSelf(0 /* flags */), EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown). - Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)"); - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.requestHideSelf( - InputMethodManager.HIDE_IMPLICIT_ONLY), - EVENT_HIDE, false /* eventExpected */, true /* shown */, - "IME is still shown after HIDE_IMPLICIT_ONLY"); - } - - // IME request to hide itself without any flags, expect hidden. - Log.i(TAG, "Call IMS#requestHideSelf(0)"); verifyInputViewStatusOnMainSync( () -> mInputMethodService.requestHideSelf(0 /* flags */), EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); - - if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // IME request to show itself with flag SHOW_IMPLICIT, expect shown. - Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)"); - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT), - EVENT_SHOW, true /* eventExpected */, true /* shown */, - "IME is shown with SHOW_IMPLICIT"); - - // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden. - Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)"); - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.requestHideSelf( - InputMethodManager.HIDE_IMPLICIT_ONLY), - EVENT_HIDE, true /* eventExpected */, false /* shown */, - "IME is not shown after HIDE_IMPLICIT_ONLY"); - } } /** @@ -289,28 +266,25 @@ public class InputMethodServiceTest { */ @Test public void testOnEvaluateInputViewShown_showImeWithHardKeyboard() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); try { config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO; - eventually(() -> - assertWithMessage("InputView should show with visible hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show with visible hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); config.keyboard = Configuration.KEYBOARD_NOKEYS; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO; - eventually(() -> - assertWithMessage("InputView should show without hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show without hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES; - eventually(() -> - assertWithMessage("InputView should show with hidden hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show with hidden hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -323,28 +297,25 @@ public class InputMethodServiceTest { */ @Test public void testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); try { config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO; - eventually(() -> - assertWithMessage("InputView should not show with visible hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isFalse()); + assertWithMessage("InputView should not show with visible hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isFalse(); config.keyboard = Configuration.KEYBOARD_NOKEYS; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO; - eventually(() -> - assertWithMessage("InputView should show without hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show without hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES; - eventually(() -> - assertWithMessage("InputView should show with hidden hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show with hidden hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -357,7 +328,7 @@ public class InputMethodServiceTest { */ @Test public void testShowSoftInput_disableShowImeWithHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -386,49 +357,17 @@ public class InputMethodServiceTest { } /** - * This checks that an explicit show request results in the IME being shown. - */ - @Test - public void testShowSoftInputExplicitly() { - setShowImeWithHardKeyboard(true /* enabled */); - - // When InputMethodService#onEvaluateInputViewShown() returns true and flag is EXPLICIT, the - // IME should be shown. - verifyInputViewStatusOnMainSync( - () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - } - - /** - * This checks that an implicit show request results in the IME being shown. - */ - @Test - public void testShowSoftInputImplicitly() { - setShowImeWithHardKeyboard(true /* enabled */); - - // When InputMethodService#onEvaluateInputViewShown() returns true and flag is IMPLICIT, - // the IME should be shown. - verifyInputViewStatusOnMainSync(() -> assertThat( - mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - } - - /** * This checks that an explicit show request when the IME is not previously shown, * and it should be shown in fullscreen mode, results in the IME being shown. */ @Test public void testShowSoftInputExplicitly_fullScreenMode() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); // Set orientation landscape to enable fullscreen mode. setOrientation(2); - eventually(() -> assertWithMessage("No longer in natural orientation") - .that(mUiDevice.isNaturalOrientation()).isFalse()); - // Wait for the TestActivity to be recreated. eventually(() -> assertWithMessage("Activity was re-created after rotation") .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); - // Get the new TestActivity. mActivity = TestActivity.getInstance(); assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull(); // Wait for the new EditText to be served by InputMethodManager. @@ -442,34 +381,40 @@ public class InputMethodServiceTest { /** * This checks that an implicit show request when the IME is not previously shown, - * and it should be shown in fullscreen mode, results in the IME not being shown. + * and it should be shown in fullscreen mode behaves like an explicit show request, resulting + * in the IME being shown. This is due to the refactor in b/298172246, causing us to lose flag + * information like {@link InputMethodManager#SHOW_IMPLICIT}. * - * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput - * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like - * SHOW_IMPLICIT. + * <p>Previously, an implicit show request when the IME is not previously shown, + * and it should be shown in fullscreen mode, would result in the IME not being shown. */ @Test - @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputImplicitly_fullScreenMode() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); // Set orientation landscape to enable fullscreen mode. setOrientation(2); - eventually(() -> assertWithMessage("No longer in natural orientation") - .that(mUiDevice.isNaturalOrientation()).isFalse()); - // Wait for the TestActivity to be recreated. eventually(() -> assertWithMessage("Activity was re-created after rotation") .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); - // Get the new TestActivity. mActivity = TestActivity.getInstance(); assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull(); // Wait for the new EditText to be served by InputMethodManager. eventually(() -> assertWithMessage("Has an input connection to the re-created Activity") .that(mImm.hasActiveInputConnection(mActivity.getEditText())).isTrue()); - verifyInputViewStatusOnMainSync(() -> assertThat( - mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown"); + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync(() -> assertThat( + mActivity.showImeWithInputMethodManager( + InputMethodManager.SHOW_IMPLICIT)) + .isTrue(), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + } else { + verifyInputViewStatusOnMainSync(() -> assertThat( + mActivity.showImeWithInputMethodManager( + InputMethodManager.SHOW_IMPLICIT)) + .isTrue(), + EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown"); + } } /** @@ -478,7 +423,7 @@ public class InputMethodServiceTest { */ @Test public void testShowSoftInputExplicitly_withHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -497,17 +442,17 @@ public class InputMethodServiceTest { } /** - * This checks that an implicit show request when a hardware keyboard is connected, - * results in the IME not being shown. + * This checks that an implicit show request when a hardware keyboard is connected behaves + * like an explicit show request, resulting in the IME being shown. This is due to the + * refactor in b/298172246, causing us to lose flag information like + * {@link InputMethodManager#SHOW_IMPLICIT}. * - * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput - * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like - * SHOW_IMPLICIT. + * <p>Previously, an implicit show request when a hardware keyboard is connected would + * result in the IME not being shown. */ @Test - @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputImplicitly_withHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -516,10 +461,20 @@ public class InputMethodServiceTest { config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES; - verifyInputViewStatusOnMainSync(() ->assertThat( - mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)) - .isTrue(), - EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown"); + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync(() -> assertThat( + mActivity.showImeWithInputMethodManager( + InputMethodManager.SHOW_IMPLICIT)) + .isTrue(), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + } else { + verifyInputViewStatusOnMainSync(() -> assertThat( + mActivity.showImeWithInputMethodManager( + InputMethodManager.SHOW_IMPLICIT)) + .isTrue(), + EVENT_SHOW, false /* eventExpected */, false /* shown */, + "IME is not shown"); + } } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -532,7 +487,7 @@ public class InputMethodServiceTest { */ @Test public void testShowSoftInputExplicitly_thenConfigurationChanged() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -565,17 +520,17 @@ public class InputMethodServiceTest { /** * This checks that an implicit show request followed by connecting a hardware keyboard - * and a configuration change, does not trigger IMS#onFinishInputView, - * but results in the IME being hidden. + * and a configuration change behaves like an explicit show request, resulting in the IME + * still being shown. This is due to the refactor in b/298172246, causing us to lose flag + * information like {@link InputMethodManager#SHOW_IMPLICIT}. * - * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput - * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like - * SHOW_IMPLICIT. + * <p>Previously, an implicit show request followed by connecting a hardware keyboard + * and a configuration change, would not trigger IMS#onFinishInputView, but resulted in the + * IME being hidden. */ @Test - @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputImplicitly_thenConfigurationChanged() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -596,16 +551,23 @@ public class InputMethodServiceTest { // Simulate a fake configuration change to avoid the recreation of TestActivity. config.orientation = Configuration.ORIENTATION_LANDSCAPE; - // Normally, IMS#onFinishInputView will be called when finishing the input view by - // the user. But if IMS#hideWindow is called when receiving a new configuration change, - // we don't expect that it's user-driven to finish the lifecycle of input view with - // IMS#onFinishInputView, because the input view will be re-initialized according - // to the last #mShowInputRequested state. So in this case we treat the input view as - // still alive. - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.onConfigurationChanged(config), - EVENT_CONFIG, true /* eventExpected */, true /* inputViewStarted */, - false /* shown */, "IME is not shown after a configuration change"); + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.onConfigurationChanged(config), + EVENT_CONFIG, true /* eventExpected */, true /* shown */, + "IME is still shown after a configuration change"); + } else { + // Normally, IMS#onFinishInputView will be called when finishing the input view by + // the user. But if IMS#hideWindow is called when receiving a new configuration + // change, we don't expect that it's user-driven to finish the lifecycle of input + // view with IMS#onFinishInputView, because the input view will be re-initialized + // according to the last #mShowInputRequested state. So in this case we treat the + // input view as still alive. + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.onConfigurationChanged(config), + EVENT_CONFIG, true /* eventExpected */, true /* inputViewStarted */, + false /* shown */, "IME is not shown after a configuration change"); + } } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -619,7 +581,7 @@ public class InputMethodServiceTest { */ @Test public void testShowSoftInputExplicitly_thenShowSoftInputImplicitly_withHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -628,12 +590,10 @@ public class InputMethodServiceTest { config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES; - // Explicit show request. verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - // Implicit show request. verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager( InputMethodManager.SHOW_IMPLICIT)).isTrue(), @@ -654,17 +614,18 @@ public class InputMethodServiceTest { /** * This checks that a forced show request directly followed by an explicit show request, - * and then a hide not always request, still results in the IME being shown - * (i.e. the explicit show request retains the forced state). + * and then a not always hide request behaves like a normal hide request, resulting in the + * IME being hidden (i.e. the explicit show request does not retain the forced state). This is + * due to the refactor in b/298172246, causing us to lose flag information like + * {@link InputMethodManager#SHOW_FORCED}. * - * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput - * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like - * HIDE_NOT_ALWAYS. + * <p>Previously, a forced show request directly followed by an explicit show request, + * and then a not always hide request, would result in the IME still being shown + * (i.e. the explicit show request would retain the forced state). */ @Test - @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputForced_testShowSoftInputExplicitly_thenHideSoftInputNotAlways() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_FORCED)).isTrue(), @@ -674,11 +635,123 @@ public class InputMethodServiceTest { mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), EVENT_SHOW, false /* eventExpected */, true /* shown */, "IME is still shown"); - verifyInputViewStatusOnMainSync(() -> assertThat( - mActivity.hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS)) - .isTrue(), - EVENT_HIDE, false /* eventExpected */, true /* shown */, - "IME is still shown after HIDE_NOT_ALWAYS"); + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync(() -> assertThat(mActivity + .hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS)) + .isTrue(), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_NOT_ALWAYS"); + } else { + verifyInputViewStatusOnMainSync(() -> assertThat(mActivity + .hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS)) + .isTrue(), + EVENT_HIDE, false /* eventExpected */, true /* shown */, + "IME is still shown after HIDE_NOT_ALWAYS"); + } + } + + /** + * This checks that an explicit show request followed by an implicit only hide request + * behaves like a normal hide request, resulting in the IME being hidden. This is due to + * the refactor in b/298172246, causing us to lose flag information like + * {@link InputMethodManager#SHOW_IMPLICIT} and {@link InputMethodManager#HIDE_IMPLICIT_ONLY}. + * + * <p>Previously, an explicit show request followed by an implicit only hide request + * would result in the IME still being shown. + */ + @Test + public void testShowSoftInputExplicitly_thenHideSoftInputImplicitOnly() { + setShowImeWithHardKeyboard(true /* enable */); + + verifyInputViewStatusOnMainSync( + () -> mActivity.showImeWithInputMethodManager(0 /* flags */), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync( + () -> mActivity.hideImeWithInputMethodManager( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_IMPLICIT_ONLY"); + } else { + verifyInputViewStatusOnMainSync( + () -> mActivity.hideImeWithInputMethodManager( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, false /* eventExpected */, true /* shown */, + "IME is still shown after HIDE_IMPLICIT_ONLY"); + } + } + + /** + * This checks that an implicit show request followed by an implicit only hide request + * results in the IME being hidden. + */ + @Test + public void testShowSoftInputImplicitly_thenHideSoftInputImplicitOnly() { + setShowImeWithHardKeyboard(true /* enable */); + + verifyInputViewStatusOnMainSync( + () -> mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT), + EVENT_SHOW, true /* eventExpected */, true /* shown */, + "IME is shown with SHOW_IMPLICIT"); + + verifyInputViewStatusOnMainSync( + () -> mActivity.hideImeWithInputMethodManager( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_IMPLICIT_ONLY"); + } + + /** + * This checks that an explicit show self request followed by an implicit only hide self request + * behaves like a normal hide self request, resulting in the IME being hidden. This is due to + * the refactor in b/298172246, causing us to lose flag information like + * {@link InputMethodManager#SHOW_IMPLICIT} and {@link InputMethodManager#HIDE_IMPLICIT_ONLY}. + * + * <p>Previously, an explicit show self request followed by an implicit only hide self request + * would result in the IME still being shown. + */ + @Test + public void testShowSelfExplicitly_thenHideSelfImplicitOnly() { + setShowImeWithHardKeyboard(true /* enable */); + + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestShowSelf(0 /* flags */), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestHideSelf( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_IMPLICIT_ONLY"); + } else { + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestHideSelf( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, false /* eventExpected */, true /* shown */, + "IME is still shown after HIDE_IMPLICIT_ONLY"); + } + } + + /** + * This checks that an implicit show self request followed by an implicit only hide self request + * results in the IME being hidden. + */ + @Test + public void testShowSelfImplicitly_thenHideSelfImplicitOnly() { + setShowImeWithHardKeyboard(true /* enable */); + + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT), + EVENT_SHOW, true /* eventExpected */, true /* shown */, + "IME is shown with SHOW_IMPLICIT"); + + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestHideSelf( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_IMPLICIT_ONLY"); } /** @@ -686,7 +759,7 @@ public class InputMethodServiceTest { */ @Test public void testFullScreenMode() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); Log.i(TAG, "Set orientation natural"); verifyFullscreenMode(() -> setOrientation(0), false /* eventExpected */, @@ -723,25 +796,22 @@ public class InputMethodServiceTest { public void testShowHideImeNavigationBar_doesDrawImeNavBar() { assumeTrue("Must have a navigation bar", hasNavigationBar()); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // Show IME verifyInputViewStatusOnMainSync( () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); + setDrawsImeNavBarAndSwitcherButton(true /* enable */); mActivity.showImeWithWindowInsetsController(); }, EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); assertWithMessage("IME navigation bar is initially shown") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue(); - // Try to hide IME nav bar mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(false /* show */)); mInstrumentation.waitForIdleSync(); assertWithMessage("IME navigation bar is not shown after hide request") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse(); - // Try to show IME nav bar mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(true /* show */)); mInstrumentation.waitForIdleSync(); assertWithMessage("IME navigation bar is shown after show request") @@ -758,25 +828,22 @@ public class InputMethodServiceTest { public void testShowHideImeNavigationBar_doesNotDrawImeNavBar() { assumeTrue("Must have a navigation bar", hasNavigationBar()); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // Show IME verifyInputViewStatusOnMainSync( () -> { - setDrawsImeNavBarAndSwitcherButton(false /* enabled */); + setDrawsImeNavBarAndSwitcherButton(false /* enable */); mActivity.showImeWithWindowInsetsController(); }, EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); assertWithMessage("IME navigation bar is initially not shown") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse(); - // Try to hide IME nav bar mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(false /* show */)); mInstrumentation.waitForIdleSync(); assertWithMessage("IME navigation bar is not shown after hide request") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse(); - // Try to show IME nav bar mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(true /* show */)); mInstrumentation.waitForIdleSync(); assertWithMessage("IME navigation bar is not shown after show request") @@ -792,7 +859,7 @@ public class InputMethodServiceTest { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( @@ -818,7 +885,7 @@ public class InputMethodServiceTest { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( @@ -844,7 +911,7 @@ public class InputMethodServiceTest { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); final var info = mImm.getCurrentInputMethodInfo(); assertWithMessage("InputMethodInfo is not null").that(info).isNotNull(); @@ -855,7 +922,7 @@ public class InputMethodServiceTest { try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); + setDrawsImeNavBarAndSwitcherButton(true /* enable */); mActivity.showImeWithWindowInsetsController(); }, EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); @@ -884,7 +951,7 @@ public class InputMethodServiceTest { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); final var info = mImm.getCurrentInputMethodInfo(); assertWithMessage("InputMethodInfo is not null").that(info).isNotNull(); @@ -893,7 +960,7 @@ public class InputMethodServiceTest { try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); + setDrawsImeNavBarAndSwitcherButton(true /* enable */); mActivity.showImeWithWindowInsetsController(); }, EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); @@ -956,7 +1023,8 @@ public class InputMethodServiceTest { runnable.run(); } mInstrumentation.waitForIdleSync(); - eventCalled = latch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); + eventCalled = latch.await(eventExpected ? EXPECT_TIMEOUT_MS : NOT_EXCEPT_TIMEOUT_MS, + TimeUnit.MILLISECONDS); } catch (InterruptedException e) { fail("Interrupted while waiting for latch: " + e.getMessage()); return; @@ -1016,10 +1084,8 @@ public class InputMethodServiceTest { verifyInputViewStatus(runnable, EVENT_CONFIG, eventExpected, false /* shown */, "IME is not shown"); if (eventExpected) { - // Wait for the TestActivity to be recreated. eventually(() -> assertWithMessage("Activity was re-created after rotation") .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); - // Get the new TestActivity. mActivity = TestActivity.getInstance(); assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull(); // Wait for the new EditText to be served by InputMethodManager. @@ -1062,6 +1128,7 @@ public class InputMethodServiceTest { private void prepareActivity() { mActivity = TestActivity.startSync(mInstrumentation); + mInstrumentation.waitForIdleSync(); Log.i(TAG, "Finish preparing activity with editor."); } @@ -1086,21 +1153,51 @@ public class InputMethodServiceTest { * @param enable the value to be set. */ private void setShowImeWithHardKeyboard(boolean enable) { + if (mInputMethodService == null) { + // If the IME is no longer around, reset the setting unconditionally. + executeShellCommand(SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD + " " + (enable ? "1" : "0")); + return; + } + final boolean currentEnabled = mInputMethodService.getShouldShowImeWithHardKeyboardForTesting(); if (currentEnabled != enable) { - executeShellCommand(enable - ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD - : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD); + executeShellCommand(SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD + " " + (enable ? "1" : "0")); eventually(() -> assertWithMessage("showImeWithHardKeyboard updated") .that(mInputMethodService.getShouldShowImeWithHardKeyboardForTesting()) .isEqualTo(enable)); } } - private static void executeShellCommand(@NonNull String cmd) { + /** + * Gets the verbose logging state in {@link android.view.inputmethod.ImeTracker}. + * + * @return {@code true} iff verbose logging is enabled. + */ + private static boolean getVerboseImeTrackerLogging() { + return executeShellCommand(GET_VERBOSE_IME_TRACKER_LOGGING_CMD).trim().equals("1"); + } + + /** + * Sets verbose logging in {@link android.view.inputmethod.ImeTracker}. + * + * @param enabled whether to enable or disable verbose logging. + * + * @implNote This must use {@link ActivityManager#notifySystemPropertiesChanged()} to listen + * for changes to the system property for the verbose ImeTracker logging. + */ + private void setVerboseImeTrackerLogging(boolean enabled) { + final var context = mInstrumentation.getContext(); + final var am = context.getSystemService(ActivityManager.class); + + executeShellCommand(SET_VERBOSE_IME_TRACKER_LOGGING_CMD + " " + (enabled ? "1" : "0")); + am.notifySystemPropertiesChanged(); + } + + @NonNull + private static String executeShellCommand(@NonNull String cmd) { Log.i(TAG, "Run command: " + cmd); - SystemUtil.runShellCommandOrThrow(cmd); + return SystemUtil.runShellCommandOrThrow(cmd); } /** @@ -1113,8 +1210,7 @@ public class InputMethodServiceTest { @NonNull private UiObject2 getUiObject(@NonNull BySelector bySelector) { - final var uiObject = mUiDevice.wait(Until.findObject(bySelector), - TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS)); + final var uiObject = mUiDevice.wait(Until.findObject(bySelector), TIMEOUT_MS); assertWithMessage("UiObject with " + bySelector + " was found").that(uiObject).isNotNull(); return uiObject; } @@ -1137,10 +1233,10 @@ public class InputMethodServiceTest { * * <p>Note, neither of these are normally drawn when in three button navigation mode. * - * @param enabled whether the IME nav bar and IME Switcher button are drawn. + * @param enable whether the IME nav bar and IME Switcher button are drawn. */ - private void setDrawsImeNavBarAndSwitcherButton(boolean enabled) { - final int flags = enabled ? IME_DRAWS_IME_NAV_BAR | SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0; + private void setDrawsImeNavBarAndSwitcherButton(boolean enable) { + final int flags = enable ? IME_DRAWS_IME_NAV_BAR | SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0; mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(flags); } diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java index 558d1a7c4e8b..d4d4dcaa4f48 100644 --- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java @@ -111,12 +111,6 @@ public class InputMethodServiceWrapper extends InputMethodService { } @Override - public void requestHideSelf(int flags) { - Log.i(TAG, "requestHideSelf() " + flags); - super.requestHideSelf(flags); - } - - @Override public void onConfigurationChanged(Configuration newConfig) { Log.i(TAG, "onConfigurationChanged() " + newConfig); super.onConfigurationChanged(newConfig); diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java index b5a538fa09f8..c7da27420cbb 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java @@ -103,7 +103,7 @@ public class AudioDeviceInventoryTest { // NOTE: for now this is only when flag asDeviceConnectionFailure is true if (asDeviceConnectionFailure()) { when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT)) + AudioSystem.AUDIO_FORMAT_DEFAULT, false /*deviceSwitch*/)) .thenReturn(AudioSystem.AUDIO_STATUS_ERROR); runWithBluetoothPrivilegedPermission( () -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo, @@ -115,7 +115,7 @@ public class AudioDeviceInventoryTest { // test that the device is added when AudioSystem returns AUDIO_STATUS_OK // when setDeviceConnectionState is called for the connection when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT)) + AudioSystem.AUDIO_FORMAT_DEFAULT, false /*deviceSwitch*/)) .thenReturn(AudioSystem.AUDIO_STATUS_OK); runWithBluetoothPrivilegedPermission( () -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo, diff --git a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java index ce59a86c6ca3..39e7d727f7c5 100644 --- a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java +++ b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java @@ -51,9 +51,9 @@ public class NoOpAudioSystemAdapter extends AudioSystemAdapter { // Overrides of AudioSystemAdapter @Override public int setDeviceConnectionState(AudioDeviceAttributes attributes, int state, - int codecFormat) { - Log.i(TAG, String.format("setDeviceConnectionState(0x%s, %d, 0x%s", - attributes.toString(), state, Integer.toHexString(codecFormat))); + int codecFormat, boolean deviceSwitch) { + Log.i(TAG, String.format("setDeviceConnectionState(0x%s, %d, 0x%s %b", + attributes.toString(), state, Integer.toHexString(codecFormat), deviceSwitch)); return AudioSystem.AUDIO_STATUS_OK; } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 82435b24dad6..6e109a8d4eaf 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -164,6 +164,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.BooleanSupplier; /** * Tests for the {@link DisplayContent} class. @@ -2674,16 +2675,67 @@ public class DisplayContentTests extends WindowTestsBase { public void testKeyguardGoingAwayWhileAodShown() { mDisplayContent.getDisplayPolicy().setAwake(true); - final WindowState appWin = newWindowBuilder("appWin", TYPE_APPLICATION).setDisplay( - mDisplayContent).build(); - final ActivityRecord activity = appWin.mActivityRecord; + final KeyguardController keyguard = mAtm.mKeyguardController; + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final int displayId = mDisplayContent.getDisplayId(); + + final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId); + final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId); + final BooleanSupplier appVisible = activity::isVisibleRequested; + + // Begin locked and in AOD + keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */); + assertFalse(keyguardGoingAway.getAsBoolean()); + assertFalse(appVisible.getAsBoolean()); + + // Start unlocking from AOD. + keyguard.keyguardGoingAway(displayId, 0x0 /* flags */); + assertTrue(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); - mAtm.mKeyguardController.setKeyguardShown(appWin.getDisplayId(), true /* keyguardShowing */, - true /* aodShowing */); - assertFalse(activity.isVisibleRequested()); + // Clear AOD. This does *not* clear the going-away status. + keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */); + assertTrue(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); + + // Finish unlock + keyguard.setKeyguardShown(displayId, false /* keyguard */, false /* aod */); + assertFalse(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); + } + + @Test + public void testKeyguardGoingAwayCanceledWhileAodShown() { + mDisplayContent.getDisplayPolicy().setAwake(true); - mAtm.mKeyguardController.keyguardGoingAway(appWin.getDisplayId(), 0 /* flags */); - assertTrue(activity.isVisibleRequested()); + final KeyguardController keyguard = mAtm.mKeyguardController; + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final int displayId = mDisplayContent.getDisplayId(); + + final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId); + final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId); + final BooleanSupplier appVisible = activity::isVisibleRequested; + + // Begin locked and in AOD + keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */); + assertFalse(keyguardGoingAway.getAsBoolean()); + assertFalse(appVisible.getAsBoolean()); + + // Start unlocking from AOD. + keyguard.keyguardGoingAway(displayId, 0x0 /* flags */); + assertTrue(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); + + // Clear AOD. This does *not* clear the going-away status. + keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */); + assertTrue(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); + + // Same API call a second time cancels the unlock, because AOD isn't changing. + keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */); + assertTrue(keyguardShowing.getAsBoolean()); + assertFalse(keyguardGoingAway.getAsBoolean()); + assertFalse(appVisible.getAsBoolean()); } @Test |