diff options
529 files changed, 14429 insertions, 5444 deletions
diff --git a/apct-tests/perftests/core/src/android/libcore/XmlSerializerPerfTest.java b/apct-tests/perftests/core/src/android/libcore/XmlSerializerPerfTest.java new file mode 100644 index 000000000000..412cb5acbf8b --- /dev/null +++ b/apct-tests/perftests/core/src/android/libcore/XmlSerializerPerfTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.libcore; + +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; +import android.test.suitebuilder.annotation.LargeTest; +import android.util.Xml; + +import androidx.test.runner.AndroidJUnit4; + +import libcore.util.XmlObjectFactory; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +/** + * Compares various kinds of method invocation. + */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class XmlSerializerPerfTest { + + @Rule + public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + @Test + public void timeFastSerializer_nonIndent_depth100() throws IOException { + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + XmlSerializer serializer = Xml.newFastSerializer(); + runTest(serializer, 100); + } + } + + @Test + public void timeFastSerializer_indent_depth100() throws IOException { + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + XmlSerializer serializer = Xml.newFastSerializer(); + serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + runTest(serializer, 100); + } + } + + @Test + public void timeKXmlSerializer_nonIndent_depth100() throws IOException { + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + XmlSerializer serializer = XmlObjectFactory.newXmlSerializer(); + runTest(serializer, 100); + } + } + + @Test + public void timeKXmlSerializer_indent_depth100() throws IOException { + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + XmlSerializer serializer = XmlObjectFactory.newXmlSerializer(); + serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + runTest(serializer, 100); + } + } + + private void runTest(XmlSerializer serializer, int depth) throws IOException { + File file = File.createTempFile(XmlSerializerPerfTest.class.getSimpleName(), "tmp"); + try (OutputStream out = new FileOutputStream(file)) { + serializer.setOutput(out, StandardCharsets.UTF_8.name()); + serializer.startDocument(null, true); + writeContent(serializer, depth); + serializer.endDocument(); + } + } + + private void writeContent(XmlSerializer serializer, int depth) throws IOException { + serializer.startTag(null, "tag"); + serializer.attribute(null, "attribute", "value1"); + if (depth > 0) { + writeContent(serializer, depth - 1); + } + serializer.endTag(null, "tag"); + } + +} diff --git a/api/Android.bp b/api/Android.bp index 9306671d758c..a3e64a565422 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -98,6 +98,7 @@ combined_apis { "framework-configinfrastructure", "framework-connectivity", "framework-connectivity-t", + "framework-devicelock", "framework-federatedcompute", "framework-graphics", "framework-healthconnect", diff --git a/boot/Android.bp b/boot/Android.bp index 9fdb9bc6506a..7839918d6a54 100644 --- a/boot/Android.bp +++ b/boot/Android.bp @@ -72,6 +72,10 @@ platform_bootclasspath { module: "com.android.conscrypt-bootclasspath-fragment", }, { + apex: "com.android.devicelock", + module: "com.android.devicelock-bootclasspath-fragment", + }, + { apex: "com.android.federatedcompute", module: "com.android.federatedcompute-bootclasspath-fragment", }, diff --git a/core/api/current.txt b/core/api/current.txt index cc1451266ac8..94d199cc00b6 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -34,6 +34,7 @@ package android { field public static final String BIND_COMPANION_DEVICE_SERVICE = "android.permission.BIND_COMPANION_DEVICE_SERVICE"; field public static final String BIND_CONDITION_PROVIDER_SERVICE = "android.permission.BIND_CONDITION_PROVIDER_SERVICE"; field public static final String BIND_CONTROLS = "android.permission.BIND_CONTROLS"; + field public static final String BIND_CREDENTIAL_PROVIDER_SERVICE = "android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"; field public static final String BIND_DEVICE_ADMIN = "android.permission.BIND_DEVICE_ADMIN"; field public static final String BIND_DREAM_SERVICE = "android.permission.BIND_DREAM_SERVICE"; field public static final String BIND_INCALL_SERVICE = "android.permission.BIND_INCALL_SERVICE"; @@ -106,6 +107,7 @@ package android { field public static final String LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK = "android.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK"; field public static final String LOADER_USAGE_STATS = "android.permission.LOADER_USAGE_STATS"; field public static final String LOCATION_HARDWARE = "android.permission.LOCATION_HARDWARE"; + field public static final String MANAGE_DEVICE_LOCK_STATE = "android.permission.MANAGE_DEVICE_LOCK_STATE"; field public static final String MANAGE_DOCUMENTS = "android.permission.MANAGE_DOCUMENTS"; field public static final String MANAGE_EXTERNAL_STORAGE = "android.permission.MANAGE_EXTERNAL_STORAGE"; field public static final String MANAGE_MEDIA = "android.permission.MANAGE_MEDIA"; @@ -9844,6 +9846,7 @@ package android.content { field public static final int CONTEXT_RESTRICTED = 4; // 0x4 field public static final String CREDENTIAL_SERVICE = "credential"; field public static final String CROSS_PROFILE_APPS_SERVICE = "crossprofileapps"; + field public static final String DEVICE_LOCK_SERVICE = "device_lock"; field public static final String DEVICE_POLICY_SERVICE = "device_policy"; field public static final String DISPLAY_HASH_SERVICE = "display_hash"; field public static final String DISPLAY_SERVICE = "display"; @@ -11990,6 +11993,7 @@ package android.content.pm { field public static final String FEATURE_CONTROLS = "android.software.controls"; field public static final String FEATURE_CREDENTIALS = "android.software.credentials"; field public static final String FEATURE_DEVICE_ADMIN = "android.software.device_admin"; + field public static final String FEATURE_DEVICE_LOCK = "android.software.device_lock"; field public static final String FEATURE_EMBEDDED = "android.hardware.type.embedded"; field public static final String FEATURE_ETHERNET = "android.hardware.ethernet"; field public static final String FEATURE_EXPANDED_PICTURE_IN_PICTURE = "android.software.expanded_picture_in_picture"; @@ -39319,6 +39323,7 @@ package android.service.notification { method public final void setNotificationsShown(String[]); method public final void snoozeNotification(String, long); method public final void updateNotificationChannel(@NonNull String, @NonNull android.os.UserHandle, @NonNull android.app.NotificationChannel); + field public static final String ACTION_SETTINGS_HOME = "android.service.notification.action.SETTINGS_HOME"; field public static final int FLAG_FILTER_TYPE_ALERTING = 2; // 0x2 field public static final int FLAG_FILTER_TYPE_CONVERSATIONS = 1; // 0x1 field public static final int FLAG_FILTER_TYPE_ONGOING = 8; // 0x8 @@ -39326,7 +39331,6 @@ package android.service.notification { field public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 4; // 0x4 field public static final int HINT_HOST_DISABLE_EFFECTS = 1; // 0x1 field public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 2; // 0x2 - field public static final String INTENT_CATEGORY_SETTINGS_HOME = "android.service.notification.category.SETTINGS_HOME"; field public static final int INTERRUPTION_FILTER_ALARMS = 4; // 0x4 field public static final int INTERRUPTION_FILTER_ALL = 1; // 0x1 field public static final int INTERRUPTION_FILTER_NONE = 3; // 0x3 @@ -41726,10 +41730,12 @@ package android.telephony { field public static final String KEY_OPPORTUNISTIC_NETWORK_PING_PONG_TIME_LONG = "opportunistic_network_ping_pong_time_long"; field public static final String KEY_PING_TEST_BEFORE_DATA_SWITCH_BOOL = "ping_test_before_data_switch_bool"; field public static final String KEY_PREFER_2G_BOOL = "prefer_2g_bool"; + field public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY = "premium_capability_maximum_notification_count_int_array"; field public static final String KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = "premium_capability_notification_backoff_hysteresis_time_millis_long"; field public static final String KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG = "premium_capability_notification_display_timeout_millis_long"; field public static final String KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = "premium_capability_purchase_condition_backoff_hysteresis_time_millis_long"; field public static final String KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING = "premium_capability_purchase_url_string"; + field public static final String KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL = "premium_capability_supported_on_lte_bool"; field public static final String KEY_PREVENT_CLIR_ACTIVATION_AND_DEACTIVATION_CODE_BOOL = "prevent_clir_activation_and_deactivation_code_bool"; field public static final String KEY_RADIO_RESTART_FAILURE_CAUSES_INT_ARRAY = "radio_restart_failure_causes_int_array"; field public static final String KEY_RCS_CONFIG_SERVER_URL_STRING = "rcs_config_server_url_string"; @@ -41793,6 +41799,8 @@ package android.telephony { field public static final String KEY_VOICEMAIL_NOTIFICATION_PERSISTENT_BOOL = "voicemail_notification_persistent_bool"; field public static final String KEY_VOICE_PRIVACY_DISABLE_UI_BOOL = "voice_privacy_disable_ui_bool"; field public static final String KEY_VOLTE_REPLACEMENT_RAT_INT = "volte_replacement_rat_int"; + field public static final String KEY_VONR_ENABLED_BOOL = "vonr_enabled_bool"; + field public static final String KEY_VONR_SETTING_VISIBILITY_BOOL = "vonr_setting_visibility_bool"; field public static final String KEY_VT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_RTT_CALL_BOOL = "vt_upgrade_supported_for_downgraded_rtt_call"; field public static final String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL = "vvm_cellular_data_required_bool"; field public static final String KEY_VVM_CLIENT_PREFIX_STRING = "vvm_client_prefix_string"; @@ -44006,7 +44014,7 @@ package android.telephony { field public static final int PHONE_TYPE_GSM = 1; // 0x1 field public static final int PHONE_TYPE_NONE = 0; // 0x0 field public static final int PHONE_TYPE_SIP = 3; // 0x3 - field public static final int PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC = 1; // 0x1 + field public static final int PREMIUM_CAPABILITY_PRIORITIZE_LATENCY = 34; // 0x22 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS = 4; // 0x4 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED = 3; // 0x3 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED = 7; // 0x7 @@ -44014,12 +44022,13 @@ package android.telephony { field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED = 10; // 0xa field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED = 13; // 0xd field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE = 12; // 0xc + field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA = 14; // 0xe + field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5; // 0x5 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11; // 0xb field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS = 1; // 0x1 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED = 2; // 0x2 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT = 9; // 0x9 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED = 6; // 0x6 - field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED = 5; // 0x5 field public static final int SET_OPPORTUNISTIC_SUB_INACTIVE_SUBSCRIPTION = 2; // 0x2 field public static final int SET_OPPORTUNISTIC_SUB_NO_OPPORTUNISTIC_SUB_AVAILABLE = 3; // 0x3 field public static final int SET_OPPORTUNISTIC_SUB_REMOTE_SERVICE_EXCEPTION = 4; // 0x4 @@ -45401,7 +45410,7 @@ package android.text { method public final int getParagraphLeft(int); method public final int getParagraphRight(int); method public float getPrimaryHorizontal(int); - method @Nullable public android.util.Range<java.lang.Integer> getRangeForRect(@NonNull android.graphics.RectF, @NonNull android.text.SegmentFinder, @NonNull android.text.Layout.TextInclusionStrategy); + method @Nullable public int[] getRangeForRect(@NonNull android.graphics.RectF, @NonNull android.text.SegmentFinder, @NonNull android.text.Layout.TextInclusionStrategy); method public float getSecondaryHorizontal(int); method public void getSelectionPath(int, int, android.graphics.Path); method public final float getSpacingAdd(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index c170f74b41c3..09b0dcaf98db 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2502,11 +2502,49 @@ package android.app.time { field @NonNull public static final android.os.Parcelable.Creator<android.app.time.ExternalTimeSuggestion> CREATOR; } + public final class TimeCapabilities implements android.os.Parcelable { + method public int describeContents(); + method public int getConfigureAutoDetectionEnabledCapability(); + method public int getSetManualTimeCapability(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeCapabilities> CREATOR; + } + + public final class TimeCapabilitiesAndConfig implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public android.app.time.TimeCapabilities getCapabilities(); + method @NonNull public android.app.time.TimeConfiguration getConfiguration(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeCapabilitiesAndConfig> CREATOR; + } + + public final class TimeConfiguration implements android.os.Parcelable { + method public int describeContents(); + method public boolean isAutoDetectionEnabled(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeConfiguration> CREATOR; + } + + public static final class TimeConfiguration.Builder { + ctor public TimeConfiguration.Builder(); + ctor public TimeConfiguration.Builder(@NonNull android.app.time.TimeConfiguration); + method @NonNull public android.app.time.TimeConfiguration build(); + method @NonNull public android.app.time.TimeConfiguration.Builder setAutoDetectionEnabled(boolean); + } + public final class TimeManager { method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public void addTimeZoneDetectorListener(@NonNull java.util.concurrent.Executor, @NonNull android.app.time.TimeManager.TimeZoneDetectorListener); + method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean confirmTime(@NonNull android.app.time.UnixEpochTime); + method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean confirmTimeZone(@NonNull String); + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeCapabilitiesAndConfig getTimeCapabilitiesAndConfig(); + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeState getTimeState(); method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeZoneCapabilitiesAndConfig getTimeZoneCapabilitiesAndConfig(); + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeZoneState getTimeZoneState(); method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public void removeTimeZoneDetectorListener(@NonNull android.app.time.TimeManager.TimeZoneDetectorListener); + method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean setManualTime(@NonNull android.app.time.UnixEpochTime); + method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean setManualTimeZone(@NonNull String); method @RequiresPermission(android.Manifest.permission.SUGGEST_EXTERNAL_TIME) public void suggestExternalTime(@NonNull android.app.time.ExternalTimeSuggestion); + method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean updateTimeConfiguration(@NonNull android.app.time.TimeConfiguration); method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean updateTimeZoneConfiguration(@NonNull android.app.time.TimeZoneConfiguration); } @@ -2514,10 +2552,19 @@ package android.app.time { method public void onChange(); } + public final class TimeState implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public android.app.time.UnixEpochTime getUnixEpochTime(); + method public boolean getUserShouldConfirmTime(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeState> CREATOR; + } + public final class TimeZoneCapabilities implements android.os.Parcelable { method public int describeContents(); method public int getConfigureAutoDetectionEnabledCapability(); method public int getConfigureGeoDetectionEnabledCapability(); + method public int getSetManualTimeZoneCapability(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeZoneCapabilities> CREATOR; } @@ -2546,6 +2593,24 @@ package android.app.time { method @NonNull public android.app.time.TimeZoneConfiguration.Builder setGeoDetectionEnabled(boolean); } + public final class TimeZoneState implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public String getId(); + method public boolean getUserShouldConfirmId(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeZoneState> CREATOR; + } + + public final class UnixEpochTime implements android.os.Parcelable { + ctor public UnixEpochTime(long, long); + method @NonNull public android.app.time.UnixEpochTime at(long); + method public int describeContents(); + method public long getElapsedRealtimeMillis(); + method public long getUnixEpochTimeMillis(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.time.UnixEpochTime> CREATOR; + } + } package android.app.usage { diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 1e4023eeee18..186e5be92524 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -134,7 +134,7 @@ package android.app { method public static void resumeAppSwitches() throws android.os.RemoteException; method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public void scheduleApplicationInfoChanged(java.util.List<java.lang.String>, int); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public void setStopUserOnSwitch(int); - method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean startUserInBackgroundOnSecondaryDisplay(int, int); + method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public boolean startUserInBackgroundOnSecondaryDisplay(int, int); method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public boolean stopUser(int, boolean); method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public boolean updateMccMncConfiguration(@NonNull String, @NonNull String); method @RequiresPermission(android.Manifest.permission.DUMP) public void waitForBroadcastIdle(); diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 576b572dcc9a..cb7b478d73b4 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -4401,7 +4401,7 @@ public class ActivityManager { */ @TestApi @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS, - android.Manifest.permission.CREATE_USERS}) + android.Manifest.permission.INTERACT_ACROSS_USERS}) public boolean startUserInBackgroundOnSecondaryDisplay(@UserIdInt int userId, int displayId) { if (!UserManager.isUsersOnSecondaryDisplaysEnabled()) { diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index c2df8022af0f..cc4650a7df71 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -16,6 +16,7 @@ package android.app; +import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; @@ -34,6 +35,10 @@ import android.os.PowerExemptionManager; import android.os.PowerExemptionManager.ReasonCode; import android.os.PowerExemptionManager.TempAllowListType; +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Objects; /** @@ -57,8 +62,11 @@ public class BroadcastOptions extends ComponentOptions { private long mRequireCompatChangeId = CHANGE_INVALID; private boolean mRequireCompatChangeEnabled = true; private boolean mIsAlarmBroadcast = false; + private boolean mIsInteractiveBroadcast = false; private long mIdForResponseEvent; private @Nullable IntentFilter mRemoveMatchingFilter; + private @DeliveryGroupPolicy int mDeliveryGroupPolicy; + private @Nullable String mDeliveryGroupKey; /** * Change ID which is invalid. @@ -161,6 +169,13 @@ public class BroadcastOptions extends ComponentOptions { "android:broadcast.is_alarm"; /** + * Corresponds to {@link #setInteractiveBroadcast(boolean)} + * @hide + */ + public static final String KEY_INTERACTIVE_BROADCAST = + "android:broadcast.is_interactive"; + + /** * @hide * @deprecated Use {@link android.os.PowerExemptionManager# * TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED} instead. @@ -190,6 +205,46 @@ public class BroadcastOptions extends ComponentOptions { private static final String KEY_REMOVE_MATCHING_FILTER = "android:broadcast.removeMatchingFilter"; + /** + * Corresponds to {@link #setDeliveryGroupPolicy(int)}. + */ + private static final String KEY_DELIVERY_GROUP_POLICY = + "android:broadcast.deliveryGroupPolicy"; + + /** + * Corresponds to {@link #setDeliveryGroupKey(String, String)}. + */ + private static final String KEY_DELIVERY_GROUP_KEY = + "android:broadcast.deliveryGroupKey"; + + /** + * The list of delivery group policies which specify how multiple broadcasts belonging to + * the same delivery group has to be handled. + * @hide + */ + @IntDef(flag = true, prefix = { "DELIVERY_GROUP_POLICY_" }, value = { + DELIVERY_GROUP_POLICY_ALL, + DELIVERY_GROUP_POLICY_MOST_RECENT, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DeliveryGroupPolicy {} + + /** + * Delivery group policy that indicates that all the broadcasts in the delivery group + * need to be delivered as is. + * + * @hide + */ + public static final int DELIVERY_GROUP_POLICY_ALL = 0; + + /** + * Delivery group policy that indicates that only the most recent broadcast in the delivery + * group need to be delivered and the rest can be dropped. + * + * @hide + */ + public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1; + public static BroadcastOptions makeBasic() { BroadcastOptions opts = new BroadcastOptions(); return opts; @@ -234,8 +289,12 @@ public class BroadcastOptions extends ComponentOptions { mRequireCompatChangeEnabled = opts.getBoolean(KEY_REQUIRE_COMPAT_CHANGE_ENABLED, true); mIdForResponseEvent = opts.getLong(KEY_ID_FOR_RESPONSE_EVENT); mIsAlarmBroadcast = opts.getBoolean(KEY_ALARM_BROADCAST, false); + mIsInteractiveBroadcast = opts.getBoolean(KEY_INTERACTIVE_BROADCAST, false); mRemoveMatchingFilter = opts.getParcelable(KEY_REMOVE_MATCHING_FILTER, IntentFilter.class); + mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY, + DELIVERY_GROUP_POLICY_ALL); + mDeliveryGroupKey = opts.getString(KEY_DELIVERY_GROUP_KEY); } /** @@ -549,6 +608,27 @@ public class BroadcastOptions extends ComponentOptions { } /** + * When set, this broadcast will be understood as having originated from + * some direct interaction by the user such as a notification tap or button + * press. Only the OS itself may use this option. + * @hide + * @param broadcastIsInteractive + * @see #isInteractiveBroadcast() + */ + public void setInteractiveBroadcast(boolean broadcastIsInteractive) { + mIsInteractiveBroadcast = broadcastIsInteractive; + } + + /** + * Did this broadcast originate with a direct user interaction? + * @return true if this broadcast is the result of an interaction, false otherwise + * @hide + */ + public boolean isInteractiveBroadcast() { + return mIsInteractiveBroadcast; + } + + /** * Did this broadcast originate from a push message from the server? * * @return true if this broadcast is a push message, false otherwise. @@ -639,6 +719,41 @@ public class BroadcastOptions extends ComponentOptions { } /** + * Set delivery group policy for this broadcast to specify how multiple broadcasts belonging to + * the same delivery group has to be handled. + * + * @hide + */ + public void setDeliveryGroupPolicy(@DeliveryGroupPolicy int policy) { + mDeliveryGroupPolicy = policy; + } + + /** @hide */ + public @DeliveryGroupPolicy int getDeliveryGroupPolicy() { + return mDeliveryGroupPolicy; + } + + /** + * Set namespace and key to identify the delivery group that this broadcast belongs to. + * If no namespace and key is set, then by default {@link Intent#filterEquals(Intent)} will be + * used to identify the delivery group. + * + * @hide + */ + public void setDeliveryGroupKey(@NonNull String namespace, @NonNull String key) { + Preconditions.checkArgument(!namespace.contains("/"), + "namespace should not contain '/'"); + Preconditions.checkArgument(!key.contains("/"), + "key should not contain '/'"); + mDeliveryGroupKey = namespace + "/" + key; + } + + /** @hide */ + public String getDeliveryGroupKey() { + return mDeliveryGroupKey; + } + + /** * Returns the created options as a Bundle, which can be passed to * {@link android.content.Context#sendBroadcast(android.content.Intent) * Context.sendBroadcast(Intent)} and related methods. @@ -658,6 +773,9 @@ public class BroadcastOptions extends ComponentOptions { if (mIsAlarmBroadcast) { b.putBoolean(KEY_ALARM_BROADCAST, true); } + if (mIsInteractiveBroadcast) { + b.putBoolean(KEY_INTERACTIVE_BROADCAST, true); + } if (mMinManifestReceiverApiLevel != 0) { b.putInt(KEY_MIN_MANIFEST_RECEIVER_API_LEVEL, mMinManifestReceiverApiLevel); } @@ -686,6 +804,12 @@ public class BroadcastOptions extends ComponentOptions { if (mRemoveMatchingFilter != null) { b.putParcelable(KEY_REMOVE_MATCHING_FILTER, mRemoveMatchingFilter); } + if (mDeliveryGroupPolicy != DELIVERY_GROUP_POLICY_ALL) { + b.putInt(KEY_DELIVERY_GROUP_POLICY, mDeliveryGroupPolicy); + } + if (mDeliveryGroupKey != null) { + b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupKey); + } return b.isEmpty() ? null : b; } } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 74eb1c526777..f9ef3cc7e1f6 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -8467,8 +8467,8 @@ public class Notification implements Parcelable } int maxAvatarSize = resources.getDimensionPixelSize( - isLowRam ? R.dimen.notification_person_icon_max_size - : R.dimen.notification_person_icon_max_size_low_ram); + isLowRam ? R.dimen.notification_person_icon_max_size_low_ram + : R.dimen.notification_person_icon_max_size); if (mUser != null && mUser.getIcon() != null) { mUser.getIcon().scaleDownIfNecessary(maxAvatarSize, maxAvatarSize); } diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 4ddfdb603e73..08a6b8c4e135 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -85,6 +85,7 @@ import android.credentials.CredentialManager; import android.credentials.ICredentialManager; import android.debug.AdbManager; import android.debug.IAdbManager; +import android.devicelock.DeviceLockFrameworkInitializer; import android.graphics.fonts.FontManager; import android.hardware.ConsumerIrManager; import android.hardware.ISerialManager; @@ -1555,6 +1556,7 @@ public final class SystemServiceRegistry { ConnectivityFrameworkInitializerTiramisu.registerServiceWrappers(); NearbyFrameworkInitializer.registerServiceWrappers(); OnDevicePersonalizationFrameworkInitializer.registerServiceWrappers(); + DeviceLockFrameworkInitializer.registerServiceWrappers(); } finally { // If any of the above code throws, we're in a pretty bad shape and the process // will likely crash, but we'll reset it just in case there's an exception handler... diff --git a/core/java/android/app/backup/BackupManager.java b/core/java/android/app/backup/BackupManager.java index 88a7c0f910d3..d2c797255b0d 100644 --- a/core/java/android/app/backup/BackupManager.java +++ b/core/java/android/app/backup/BackupManager.java @@ -29,7 +29,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Build; -import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.RemoteException; @@ -1123,18 +1122,4 @@ public class BackupManager { }); } } - - private class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub { - final BackupManagerMonitor mMonitor; - - BackupManagerMonitorWrapper(BackupManagerMonitor monitor) { - mMonitor = monitor; - } - - @Override - public void onEvent(final Bundle event) throws RemoteException { - mMonitor.onEvent(event); - } - } - } diff --git a/core/java/android/app/backup/BackupManagerMonitorWrapper.java b/core/java/android/app/backup/BackupManagerMonitorWrapper.java new file mode 100644 index 000000000000..0b189958ef0a --- /dev/null +++ b/core/java/android/app/backup/BackupManagerMonitorWrapper.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.backup; + +import android.os.Bundle; +import android.os.RemoteException; + +/** + * Wrapper around {@link BackupManagerMonitor} that helps with IPC between the caller of backup + * APIs and the backup service. + * + * The caller implements {@link BackupManagerMonitor} and passes it into framework APIs that run on + * the caller's process. Those framework APIs will then wrap it around this class when doing the + * actual IPC. + */ +class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub { + private final BackupManagerMonitor mMonitor; + + BackupManagerMonitorWrapper(BackupManagerMonitor monitor) { + mMonitor = monitor; + } + + @Override + public void onEvent(final Bundle event) throws RemoteException { + mMonitor.onEvent(event); + } +} diff --git a/core/java/android/app/backup/BackupRestoreEventLogger.java b/core/java/android/app/backup/BackupRestoreEventLogger.java new file mode 100644 index 000000000000..b789b38c966e --- /dev/null +++ b/core/java/android/app/backup/BackupRestoreEventLogger.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.backup; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +// TODO(b/244436184): Make this @SystemApi +/** + * Class to log B&R stats for each data type that is backed up and restored by the calling app. + * + * The logger instance is designed to accept a limited number of unique + * {link @BackupRestoreDataType} values, as determined by the underlying implementation. Apps are + * expected to have a small pre-defined set of data type values they use. Attempts to log too many + * unique values will be rejected. + * + * @hide + */ +public class BackupRestoreEventLogger { + /** + * Max number of unique data types for which an instance of this logger can store info. Attempts + * to use more distinct data type values will be rejected. + */ + public static final int DATA_TYPES_ALLOWED = 15; + + /** + * Operation types for which this logger can be used. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + OperationType.BACKUP, + OperationType.RESTORE + }) + @interface OperationType { + int BACKUP = 1; + int RESTORE = 2; + } + + /** + * Denotes that the annotated element identifies a data type as required by the logging methods + * of {@code BackupRestoreEventLogger} + */ + @Retention(RetentionPolicy.SOURCE) + public @interface BackupRestoreDataType {} + + /** + * Denotes that the annotated element identifies an error type as required by the logging + * methods of {@code BackupRestoreEventLogger} + */ + @Retention(RetentionPolicy.SOURCE) + public @interface BackupRestoreError {} + + private final int mOperationType; + + /** + * @param operationType type of the operation for which logging will be performed. See + * {@link OperationType}. Attempts to use logging methods that don't match + * the specified operation type will be rejected (e.g. use backup methods + * for a restore logger and vice versa). + */ + public BackupRestoreEventLogger(@OperationType int operationType) { + mOperationType = operationType; + } + + /** + * Report progress during a backup operation. Call this method for each distinct data type that + * your {@code BackupAgent} implementation handles for any items of that type that have been + * successfully backed up. Repeated calls to this method with the same {@code dataType} will + * increase the total count of items associated with this data type by {@code count}. + * + * This method should be called from a {@link BackupAgent} implementation during an ongoing + * backup operation. + * + * @param dataType the type of data being backed. + * @param count number of items of the given type that have been successfully backed up. + * + * @return boolean, indicating whether the log has been accepted. + */ + public boolean logItemsBackedUp(@NonNull @BackupRestoreDataType String dataType, int count) { + return true; + } + + /** + * Report errors during a backup operation. Call this method whenever items of a certain data + * type failed to back up. Repeated calls to this method with the same {@code dataType} / + * {@code error} will increase the total count of items associated with this data type / error + * by {@code count}. + * + * This method should be called from a {@link BackupAgent} implementation during an ongoing + * backup operation. + * + * @param dataType the type of data being backed. + * @param count number of items of the given type that have failed to back up. + * @param error optional, the error that has caused the failure. + * + * @return boolean, indicating whether the log has been accepted. + */ + public boolean logItemsBackupFailed(@NonNull @BackupRestoreDataType String dataType, int count, + @Nullable @BackupRestoreError String error) { + return true; + } + + /** + * Report metadata associated with a data type that is currently being backed up, e.g. name of + * the selected wallpaper file / package. Repeated calls to this method with the same {@code + * dataType} will overwrite the previously supplied {@code metaData} value. + * + * The logger does not store or transmit the provided metadata value. Instead, it’s replaced + * with the SHA-256 hash of the provided string. + * + * This method should be called from a {@link BackupAgent} implementation during an ongoing + * backup operation. + * + * @param dataType the type of data being backed up. + * @param metaData the metadata associated with the data type. + * + * @return boolean, indicating whether the log has been accepted. + */ + public boolean logBackupMetaData(@NonNull @BackupRestoreDataType String dataType, + @NonNull String metaData) { + return true; + } + + /** + * Report progress during a restore operation. Call this method for each distinct data type that + * your {@code BackupAgent} implementation handles if any items of that type have been + * successfully restored. Repeated calls to this method with the same {@code dataType} will + * increase the total count of items associated with this data type by {@code count}. + * + * This method should either be called from a {@link BackupAgent} implementation during an + * ongoing restore operation or during any delayed restore actions the package had scheduled + * earlier (e.g. complete the restore once a certain dependency becomes available on the + * device). + * + * @param dataType the type of data being restored. + * @param count number of items of the given type that have been successfully restored. + * + * @return boolean, indicating whether the log has been accepted. + */ + public boolean logItemsRestored(@NonNull @BackupRestoreDataType String dataType, int count) { + return true; + } + + /** + * Report errors during a restore operation. Call this method whenever items of a certain data + * type failed to restore. Repeated calls to this method with the same {@code dataType} / + * {@code error} will increase the total count of items associated with this data type / error + * by {@code count}. + * + * This method should either be called from a {@link BackupAgent} implementation during an + * ongoing restore operation or during any delayed restore actions the package had scheduled + * earlier (e.g. complete the restore once a certain dependency becomes available on the + * device). + * + * @param dataType the type of data being restored. + * @param count number of items of the given type that have failed to restore. + * @param error optional, the error that has caused the failure. + * + * @return boolean, indicating whether the log has been accepted. + */ + public boolean logItemsRestoreFailed(@NonNull @BackupRestoreDataType String dataType, int count, + @Nullable @BackupRestoreError String error) { + return true; + } + + /** + * Report metadata associated with a data type that is currently being restored, e.g. name of + * the selected wallpaper file / package. Repeated calls to this method with the same + * {@code dataType} will overwrite the previously supplied {@code metaData} value. + * + * The logger does not store or transmit the provided metadata value. Instead, it’s replaced + * with the SHA-256 hash of the provided string. + * + * This method should either be called from a {@link BackupAgent} implementation during an + * ongoing restore operation or during any delayed restore actions the package had scheduled + * earlier (e.g. complete the restore once a certain dependency becomes available on the + * device). + * + * @param dataType the type of data being restored. + * @param metadata the metadata associated with the data type. + * + * @return boolean, indicating whether the log has been accepted. + */ + public boolean logRestoreMetadata(@NonNull @BackupRestoreDataType String dataType, + @NonNull String metadata) { + return true; + } + + /** + * Get the contents of this logger. This method should only be used by B&R code in Android + * Framework. + * + * @hide + */ + public List<DataTypeResult> getLoggingResults() { + return Collections.emptyList(); + } + + /** + * Get the operation type for which this logger was created. This method should only be used + * by B&R code in Android Framework. + * + * @hide + */ + public @OperationType int getOperationType() { + return mOperationType; + } + + /** + * Encapsulate logging results for a single data type. + */ + public static class DataTypeResult { + @BackupRestoreDataType + private final String mDataType; + private final int mSuccessCount; + private final Map<String, Integer> mErrors; + private final byte[] mMetadataHash; + + public DataTypeResult(String dataType, int successCount, + Map<String, Integer> errors, byte[] metadataHash) { + mDataType = dataType; + mSuccessCount = successCount; + mErrors = errors; + mMetadataHash = metadataHash; + } + + @NonNull + @BackupRestoreDataType + public String getDataType() { + return mDataType; + } + + /** + * @return number of items of the given data type that have been successfully backed up or + * restored. + */ + public int getSuccessCount() { + return mSuccessCount; + } + + /** + * @return mapping of {@link BackupRestoreError} to the count of items that are affected by + * the error. + */ + @NonNull + public Map<String, Integer> getErrors() { + return mErrors; + } + + /** + * @return SHA-256 hash of the metadata or {@code null} of no metadata has been logged for + * this data type. + */ + @Nullable + public byte[] getMetadataHash() { + return mMetadataHash; + } + } +} diff --git a/core/java/android/app/backup/BackupTransport.java b/core/java/android/app/backup/BackupTransport.java index f6de72b43de6..90e9df47bc4b 100644 --- a/core/java/android/app/backup/BackupTransport.java +++ b/core/java/android/app/backup/BackupTransport.java @@ -656,6 +656,20 @@ public class BackupTransport { } /** + * Ask the transport for a {@link IBackupManagerMonitor} instance which will be used by the + * framework to report logging events back to the transport. + * + * <p>Backups requested from outside the framework may pass in a monitor with the request, + * however backups initiated by the framework will call this method to retrieve one. + * + * @hide + */ + @Nullable + public BackupManagerMonitor getBackupManagerMonitor() { + return null; + } + + /** * Bridge between the actual IBackupTransport implementation and the stable API. If the * binder interface needs to change, we use this layer to translate so that we can * (if appropriate) decouple those framework-side changes from the BackupTransport @@ -952,5 +966,15 @@ public class BackupTransport { callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); } } + + @Override + public void getBackupManagerMonitor(AndroidFuture<IBackupManagerMonitor> resultFuture) { + try { + BackupManagerMonitor result = BackupTransport.this.getBackupManagerMonitor(); + resultFuture.complete(new BackupManagerMonitorWrapper(result)); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } + } } } diff --git a/core/java/android/app/backup/RestoreSession.java b/core/java/android/app/backup/RestoreSession.java index 933670415f2e..fe68ec193642 100644 --- a/core/java/android/app/backup/RestoreSession.java +++ b/core/java/android/app/backup/RestoreSession.java @@ -20,7 +20,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.content.Context; -import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.RemoteException; @@ -393,17 +392,4 @@ public class RestoreSession { mHandler.obtainMessage(MSG_RESTORE_FINISHED, error, 0)); } } - - private class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub { - final BackupManagerMonitor mMonitor; - - BackupManagerMonitorWrapper(BackupManagerMonitor monitor) { - mMonitor = monitor; - } - - @Override - public void onEvent(final Bundle event) throws RemoteException { - mMonitor.onEvent(event); - } - } } diff --git a/core/java/android/app/time/TimeCapabilities.java b/core/java/android/app/time/TimeCapabilities.java index 76bad58e924b..752caac0c5cd 100644 --- a/core/java/android/app/time/TimeCapabilities.java +++ b/core/java/android/app/time/TimeCapabilities.java @@ -20,6 +20,7 @@ import static android.app.time.Capabilities.CAPABILITY_NOT_APPLICABLE; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.app.time.Capabilities.CapabilityState; import android.os.Parcel; import android.os.Parcelable; @@ -37,6 +38,7 @@ import java.util.Objects; * * @hide */ +@SystemApi public final class TimeCapabilities implements Parcelable { public static final @NonNull Creator<TimeCapabilities> CREATOR = new Creator<>() { diff --git a/core/java/android/app/time/TimeCapabilitiesAndConfig.java b/core/java/android/app/time/TimeCapabilitiesAndConfig.java index b6a081825757..c9a45e04227a 100644 --- a/core/java/android/app/time/TimeCapabilitiesAndConfig.java +++ b/core/java/android/app/time/TimeCapabilitiesAndConfig.java @@ -17,6 +17,7 @@ package android.app.time; import android.annotation.NonNull; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; @@ -27,6 +28,7 @@ import java.util.Objects; * * @hide */ +@SystemApi public final class TimeCapabilitiesAndConfig implements Parcelable { public static final @NonNull Creator<TimeCapabilitiesAndConfig> CREATOR = diff --git a/core/java/android/app/time/TimeConfiguration.java b/core/java/android/app/time/TimeConfiguration.java index 7d986983160e..048f85a1e1a4 100644 --- a/core/java/android/app/time/TimeConfiguration.java +++ b/core/java/android/app/time/TimeConfiguration.java @@ -18,6 +18,7 @@ package android.app.time; import android.annotation.NonNull; import android.annotation.StringDef; +import android.annotation.SystemApi; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -40,6 +41,7 @@ import java.util.Objects; * * @hide */ +@SystemApi public final class TimeConfiguration implements Parcelable { public static final @NonNull Creator<TimeConfiguration> CREATOR = @@ -155,6 +157,7 @@ public final class TimeConfiguration implements Parcelable { * * @hide */ + @SystemApi public static final class Builder { private final Bundle mBundle = new Bundle(); diff --git a/core/java/android/app/time/TimeManager.java b/core/java/android/app/time/TimeManager.java index 9f66f094786b..e35e359424e2 100644 --- a/core/java/android/app/time/TimeManager.java +++ b/core/java/android/app/time/TimeManager.java @@ -88,8 +88,6 @@ public final class TimeManager { /** * Returns the calling user's time capabilities and configuration. - * - * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) @NonNull @@ -107,10 +105,26 @@ public final class TimeManager { /** * Modifies the time detection configuration. * - * @return {@code true} if all the configuration settings specified have been set to the - * new values, {@code false} if none have + * <p>The ability to modify configuration settings can be subject to restrictions. For + * example, they may be determined by device hardware, general policy (i.e. only the primary + * user can set them), or by a managed device policy. Use {@link + * #getTimeCapabilitiesAndConfig()} to obtain information at runtime about the user's + * capabilities. + * + * <p>Attempts to modify configuration settings with capabilities that are {@link + * Capabilities#CAPABILITY_NOT_SUPPORTED} or {@link + * Capabilities#CAPABILITY_NOT_ALLOWED} will have no effect and a {@code false} + * will be returned. Modifying configuration settings with capabilities that are {@link + * Capabilities#CAPABILITY_NOT_APPLICABLE} or {@link + * Capabilities#CAPABILITY_POSSESSED} will succeed. See {@link + * TimeZoneCapabilities} for further details. * - * @hide + * <p>If the supplied configuration only has some values set, then only the specified settings + * will be updated (where the user's capabilities allow) and other settings will be left + * unchanged. + * + * @return {@code true} if all the configuration settings specified have been set to the + * new values, {@code false} if none have */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean updateTimeConfiguration(@NonNull TimeConfiguration configuration) { @@ -280,8 +294,6 @@ public final class TimeManager { /** * Returns a snapshot of the device's current system clock time state. See also {@link * #confirmTime(UnixEpochTime)} for how this information can be used. - * - * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) @NonNull @@ -306,8 +318,6 @@ public final class TimeManager { * <p>Returns {@code false} if the confirmation is invalid, i.e. if the time being * confirmed is no longer the time the device is currently set to. Confirming a time * in which the system already has high confidence will return {@code true}. - * - * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean confirmTime(@NonNull UnixEpochTime unixEpochTime) { @@ -329,8 +339,6 @@ public final class TimeManager { * capabilities prevents the time being accepted, e.g. if the device is currently set to * "automatic time detection". This method returns {@code true} if the time was accepted even * if it is the same as the current device time. - * - * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean setManualTime(@NonNull UnixEpochTime unixEpochTime) { @@ -353,8 +361,6 @@ public final class TimeManager { * Returns a snapshot of the device's current time zone state. See also {@link * #confirmTimeZone(String)} and {@link #setManualTimeZone(String)} for how this information may * be used. - * - * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) @NonNull @@ -379,8 +385,6 @@ public final class TimeManager { * <p>Returns {@code false} if the confirmation is invalid, i.e. if the time zone ID being * confirmed is no longer the time zone ID the device is currently set to. Confirming a time * zone ID in which the system already has high confidence returns {@code true}. - * - * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean confirmTimeZone(@NonNull String timeZoneId) { @@ -402,8 +406,6 @@ public final class TimeManager { * capabilities prevents the time zone being accepted, e.g. if the device is currently set to * "automatic time zone detection". {@code true} is returned if the time zone is accepted. A * time zone that is accepted and matches the current device time zone returns {@code true}. - * - * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean setManualTimeZone(@NonNull String timeZoneId) { diff --git a/core/java/android/app/time/TimeState.java b/core/java/android/app/time/TimeState.java index 01c869d99338..c209cde2cf49 100644 --- a/core/java/android/app/time/TimeState.java +++ b/core/java/android/app/time/TimeState.java @@ -18,6 +18,7 @@ package android.app.time; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; import android.os.ShellCommand; @@ -36,6 +37,7 @@ import java.util.Objects; * * @hide */ +@SystemApi public final class TimeState implements Parcelable { public static final @NonNull Creator<TimeState> CREATOR = new Creator<>() { diff --git a/core/java/android/app/time/TimeZoneCapabilities.java b/core/java/android/app/time/TimeZoneCapabilities.java index 2f147cef9ffe..b647fc33055d 100644 --- a/core/java/android/app/time/TimeZoneCapabilities.java +++ b/core/java/android/app/time/TimeZoneCapabilities.java @@ -114,8 +114,6 @@ public final class TimeZoneCapabilities implements Parcelable { * <p>The time zone will be ignored in all cases unless the value is {@link * Capabilities#CAPABILITY_POSSESSED}. See also * {@link TimeZoneConfiguration#isAutoDetectionEnabled()}. - * - * @hide */ @CapabilityState public int getSetManualTimeZoneCapability() { diff --git a/core/java/android/app/time/TimeZoneState.java b/core/java/android/app/time/TimeZoneState.java index 8e87111986ce..beb6dc6d1dfb 100644 --- a/core/java/android/app/time/TimeZoneState.java +++ b/core/java/android/app/time/TimeZoneState.java @@ -18,6 +18,7 @@ package android.app.time; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; import android.os.ShellCommand; @@ -36,6 +37,7 @@ import java.util.Objects; * * @hide */ +@SystemApi public final class TimeZoneState implements Parcelable { public static final @NonNull Creator<TimeZoneState> CREATOR = new Creator<>() { diff --git a/core/java/android/app/time/UnixEpochTime.java b/core/java/android/app/time/UnixEpochTime.java index 576bf6453eca..3a35f3cd1acb 100644 --- a/core/java/android/app/time/UnixEpochTime.java +++ b/core/java/android/app/time/UnixEpochTime.java @@ -19,6 +19,7 @@ package android.app.time; import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; import android.os.ShellCommand; @@ -38,6 +39,7 @@ import java.util.Objects; * * @hide */ +@SystemApi public final class UnixEpochTime implements Parcelable { @ElapsedRealtimeLong private final long mElapsedRealtimeMillis; private final long mUnixEpochTimeMillis; @@ -153,9 +155,8 @@ public final class UnixEpochTime implements Parcelable { * Creates a new Unix epoch time value at {@code elapsedRealtimeTimeMillis} by adjusting this * Unix epoch time by the difference between the elapsed realtime value supplied and the one * associated with this instance. - * - * @hide */ + @NonNull public UnixEpochTime at(@ElapsedRealtimeLong long elapsedRealtimeTimeMillis) { long adjustedUnixEpochTimeMillis = (elapsedRealtimeTimeMillis - mElapsedRealtimeMillis) + mUnixEpochTimeMillis; diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java index cc303fb1f413..24e47bf9e47c 100644 --- a/core/java/android/appwidget/AppWidgetHost.java +++ b/core/java/android/appwidget/AppWidgetHost.java @@ -418,14 +418,7 @@ public class AppWidgetHost { AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget); view.setInteractionHandler(mInteractionHandler); view.setAppWidget(appWidgetId, appWidget); - addListener(appWidgetId, view); - RemoteViews views; - try { - views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId); - } catch (RemoteException e) { - throw new RuntimeException("system server dead?", e); - } - view.updateAppWidget(views); + setListener(appWidgetId, view); return view; } @@ -513,13 +506,19 @@ public class AppWidgetHost { * The AppWidgetHost retains a pointer to the newly-created listener. * @param appWidgetId The ID of the app widget for which to add the listener * @param listener The listener interface that deals with actions towards the widget view - * * @hide */ - public void addListener(int appWidgetId, @NonNull AppWidgetHostListener listener) { + public void setListener(int appWidgetId, @NonNull AppWidgetHostListener listener) { synchronized (mListeners) { mListeners.put(appWidgetId, listener); } + RemoteViews views = null; + try { + views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId); + } catch (RemoteException e) { + throw new RuntimeException("system server dead?", e); + } + listener.updateAppWidget(views); } /** diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl index e7f191665302..295d69d4b27d 100644 --- a/core/java/android/companion/virtual/IVirtualDevice.aidl +++ b/core/java/android/companion/virtual/IVirtualDevice.aidl @@ -88,6 +88,7 @@ interface IVirtualDevice { IBinder token, in Point screenSize); void unregisterInputDevice(IBinder token); + int getInputDeviceId(IBinder token); boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event); boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event); boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event); diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 753c93612f40..d65210b8a0bc 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -3938,6 +3938,7 @@ public abstract class Context { //@hide: SAFETY_CENTER_SERVICE, DISPLAY_HASH_SERVICE, CREDENTIAL_SERVICE, + DEVICE_LOCK_SERVICE, }) @Retention(RetentionPolicy.SOURCE) public @interface ServiceName {} @@ -6073,6 +6074,14 @@ public abstract class Context { public static final String CREDENTIAL_SERVICE = "credential"; /** + * Use with {@link #getSystemService(String)} to retrieve a + * {@link android.devicelock.DeviceLockManager}. + * + * @see #getSystemService(String) + */ + public static final String DEVICE_LOCK_SERVICE = "device_lock"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index db991dcd3afc..823c14281818 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -4194,6 +4194,14 @@ public abstract class PackageManager { @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_CREDENTIALS = "android.software.credentials"; + /** + * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: + * The device supports locking (for example, by a financing provider in case of a missed + * payment). + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_DEVICE_LOCK = "android.software.device_lock"; + /** @hide */ public static final boolean APP_ENUMERATION_ENABLED_BY_DEFAULT = true; diff --git a/core/java/android/credentials/ui/Constants.java b/core/java/android/credentials/ui/Constants.java new file mode 100644 index 000000000000..aeeede744188 --- /dev/null +++ b/core/java/android/credentials/ui/Constants.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials.ui; + +/** + * Constants for the ui protocol that doesn't fit into other individual data structures. + * + * @hide + */ +public class Constants { + + /** + * The intent extra key for the {@code ResultReceiver} object when launching the UX + * activities. + */ + public static final String EXTRA_RESULT_RECEIVER = + "android.credentials.ui.extra.RESULT_RECEIVER"; + +} diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java index 122c54ad8144..b9ee72dcdcf8 100644 --- a/core/java/android/credentials/ui/Entry.java +++ b/core/java/android/credentials/ui/Entry.java @@ -30,12 +30,39 @@ import com.android.internal.util.AnnotationValidations; * @hide */ public class Entry implements Parcelable { - // TODO: move to jetpack. + // TODO: these constants should go to jetpack. public static final String VERSION = "v1"; public static final Uri CREDENTIAL_MANAGER_ENTRY_URI = Uri.parse("credentialmanager.slice"); - public static final String HINT_TITLE = "hint_title"; - public static final String HINT_SUBTITLE = "hint_subtitle"; - public static final String HINT_ICON = "hint_icon"; + // TODO: remove these hint constants and use the credential entry & action ones defined below. + public static final String HINT_TITLE = "HINT_TITLE"; + public static final String HINT_SUBTITLE = "HINT_SUBTITLE"; + public static final String HINT_ICON = "HINT_ICON"; + /** + * 1. CREDENTIAL ENTRY CONSTANTS + */ + // User profile picture associated with this credential entry. + public static final String HINT_PROFILE_ICON = "HINT_PROFILE_ICON"; + public static final String HINT_CREDENTIAL_TYPE_ICON = "HINT_CREDENTIAL_TYPE_ICON"; + // The user account name of this provider app associated with this entry. + // Note: this is independent from the request app. + public static final String HINT_USER_PROVIDER_ACCOUNT_NAME = "HINT_USER_PROVIDER_ACCOUNT_NAME"; + public static final String HINT_PASSWORD_COUNT = "HINT_PASSWORD_COUNT"; + public static final String HINT_PASSKEY_COUNT = "HINT_PASSKEY_COUNT"; + public static final String HINT_TOTAL_CREDENTIAL_COUNT = "HINT_TOTAL_CREDENTIAL_COUNT"; + public static final String HINT_LAST_USED_TIME_MILLIS = "HINT_LAST_USED_TIME_MILLIS"; + /** Below are only available for get flows. */ + public static final String HINT_NOTE = "HINT_NOTE"; + public static final String HINT_USER_NAME = "HINT_USER_NAME"; + public static final String HINT_CREDENTIAL_TYPE = "HINT_CREDENTIAL_TYPE"; + public static final String HINT_PASSKEY_USER_DISPLAY_NAME = "HINT_PASSKEY_USER_DISPLAY_NAME"; + public static final String HINT_PASSWORD_VALUE = "HINT_PASSWORD_VALUE"; + + /** + * 2. ACTION CONSTANTS + */ + public static final String HINT_ACTION_TITLE = "HINT_ACTION_TITLE"; + public static final String HINT_ACTION_SUBTEXT = "HINT_ACTION_SUBTEXT"; + public static final String HINT_ACTION_ICON = "HINT_ACTION_ICON"; /** * The intent extra key for the action chip {@code Entry} list when launching the UX activities. @@ -55,7 +82,7 @@ public class Entry implements Parcelable { public static final String EXTRA_ENTRY_AUTHENTICATION_ACTION = "android.credentials.ui.extra.ENTRY_AUTHENTICATION_ACTION"; - // TODO: may be changed to other type depending on the service implementation. + // TODO: change to string key + string subkey. private final int mId; @NonNull diff --git a/core/java/android/credentials/ui/IntentFactory.java b/core/java/android/credentials/ui/IntentFactory.java new file mode 100644 index 000000000000..9a038d137434 --- /dev/null +++ b/core/java/android/credentials/ui/IntentFactory.java @@ -0,0 +1,68 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials.ui; + +import android.content.ComponentName; +import android.content.Intent; +import android.os.Parcel; +import android.os.ResultReceiver; + +import java.util.ArrayList; + +/** + * Helpers for generating the intents and related extras parameters to launch the UI activities. + * + * @hide + */ +public class IntentFactory { + /** Generate a new launch intent to the . */ + public static Intent newIntent(RequestInfo requestInfo, + ArrayList<ProviderData> providerDataList, ResultReceiver resultReceiver) { + Intent intent = new Intent(); + // TODO: define these as proper config strings. + String activityName = "com.androidauth.tatiaccountselector/.CredentialSelectorActivity"; + // String activityName = "com.android.credentialmanager/.CredentialSelectorActivity"; + intent.setComponent(ComponentName.unflattenFromString(activityName)); + + intent.putParcelableArrayListExtra( + ProviderData.EXTRA_PROVIDER_DATA_LIST, providerDataList); + intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo); + intent.putExtra(Constants.EXTRA_RESULT_RECEIVER, + toIpcFriendlyResultReceiver(resultReceiver)); + + return intent; + } + + /** + * Convert an instance of a "locally-defined" ResultReceiver to an instance of + * {@link android.os.ResultReceiver} itself, which the receiving process will be able to + * unmarshall. + */ + private static <T extends ResultReceiver> ResultReceiver toIpcFriendlyResultReceiver( + T resultReceiver) { + final Parcel parcel = Parcel.obtain(); + resultReceiver.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel); + parcel.recycle(); + + return ipcFriendly; + } + + private IntentFactory() {} +} diff --git a/core/java/android/credentials/ui/ProviderData.java b/core/java/android/credentials/ui/ProviderData.java index 38bd4e57b758..35e12fa43b28 100644 --- a/core/java/android/credentials/ui/ProviderData.java +++ b/core/java/android/credentials/ui/ProviderData.java @@ -16,8 +16,10 @@ package android.credentials.ui; +import android.annotation.CurrentTimeMillisLong; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.drawable.Icon; import android.os.Parcel; import android.os.Parcelable; @@ -43,30 +45,49 @@ public class ProviderData implements Parcelable { @NonNull private final String mProviderId; @NonNull + private final String mProviderDisplayName; + @NonNull + private final Icon mIcon; + @NonNull private final List<Entry> mCredentialEntries; @NonNull private final List<Entry> mActionChips; @Nullable private final Entry mAuthenticationEntry; + private final @CurrentTimeMillisLong long mLastUsedTimeMillis; + public ProviderData( - @NonNull String providerId, - @NonNull List<Entry> credentialEntries, - @NonNull List<Entry> actionChips, - @Nullable Entry authenticationEntry) { + @NonNull String providerId, @NonNull String providerDisplayName, + @NonNull Icon icon, @NonNull List<Entry> credentialEntries, + @NonNull List<Entry> actionChips, @Nullable Entry authenticationEntry, + @CurrentTimeMillisLong long lastUsedTimeMillis) { mProviderId = providerId; + mProviderDisplayName = providerDisplayName; + mIcon = icon; mCredentialEntries = credentialEntries; mActionChips = actionChips; mAuthenticationEntry = authenticationEntry; + mLastUsedTimeMillis = lastUsedTimeMillis; } - /** Returns the provider package name. */ + /** Returns the unique provider id. */ @NonNull public String getProviderId() { return mProviderId; } @NonNull + public String getProviderDisplayName() { + return mProviderDisplayName; + } + + @NonNull + public Icon getIcon() { + return mIcon; + } + + @NonNull public List<Entry> getCredentialEntries() { return mCredentialEntries; } @@ -81,11 +102,24 @@ public class ProviderData implements Parcelable { return mAuthenticationEntry; } + /** Returns the time when the provider was last used. */ + public @CurrentTimeMillisLong long getLastUsedTimeMillis() { + return mLastUsedTimeMillis; + } + protected ProviderData(@NonNull Parcel in) { String providerId = in.readString8(); mProviderId = providerId; AnnotationValidations.validate(NonNull.class, null, mProviderId); + String providerDisplayName = in.readString8(); + mProviderDisplayName = providerDisplayName; + AnnotationValidations.validate(NonNull.class, null, mProviderDisplayName); + + Icon icon = in.readTypedObject(Icon.CREATOR); + mIcon = icon; + AnnotationValidations.validate(NonNull.class, null, mIcon); + List<Entry> credentialEntries = new ArrayList<>(); in.readTypedList(credentialEntries, Entry.CREATOR); mCredentialEntries = credentialEntries; @@ -98,14 +132,20 @@ public class ProviderData implements Parcelable { Entry authenticationEntry = in.readTypedObject(Entry.CREATOR); mAuthenticationEntry = authenticationEntry; + + long lastUsedTimeMillis = in.readLong(); + mLastUsedTimeMillis = lastUsedTimeMillis; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString8(mProviderId); + dest.writeString8(mProviderDisplayName); + dest.writeTypedObject(mIcon, flags); dest.writeTypedList(mCredentialEntries); dest.writeTypedList(mActionChips); dest.writeTypedObject(mAuthenticationEntry, flags); + dest.writeLong(mLastUsedTimeMillis); } @Override @@ -124,4 +164,83 @@ public class ProviderData implements Parcelable { return new ProviderData[size]; } }; + + /** + * Builder for {@link ProviderData}. + * + * @hide + */ + public static class Builder { + private @NonNull String mProviderId; + private @NonNull String mProviderDisplayName; + private @NonNull Icon mIcon; + private @NonNull List<Entry> mCredentialEntries = new ArrayList<>(); + private @NonNull List<Entry> mActionChips = new ArrayList<>(); + private @Nullable Entry mAuthenticationEntry = null; + private @CurrentTimeMillisLong long mLastUsedTimeMillis = 0L; + + /** Constructor with required properties. */ + public Builder(@NonNull String providerId, @NonNull String providerDisplayName, + @NonNull Icon icon) { + mProviderId = providerId; + mProviderDisplayName = providerDisplayName; + mIcon = icon; + } + + /** Sets the unique provider id. */ + @NonNull + public Builder setProviderId(@NonNull String providerId) { + mProviderId = providerId; + return this; + } + + /** Sets the provider display name to be displayed to the user. */ + @NonNull + public Builder setProviderDisplayName(@NonNull String providerDisplayName) { + mProviderDisplayName = providerDisplayName; + return this; + } + + /** Sets the provider icon to be displayed to the user. */ + @NonNull + public Builder setIcon(@NonNull Icon icon) { + mIcon = icon; + return this; + } + + /** Sets the list of save / get credential entries to be displayed to the user. */ + @NonNull + public Builder setCredentialEntries(@NonNull List<Entry> credentialEntries) { + mCredentialEntries = credentialEntries; + return this; + } + + /** Sets the list of action chips to be displayed to the user. */ + @NonNull + public Builder setActionChips(@NonNull List<Entry> actionChips) { + mActionChips = actionChips; + return this; + } + + /** Sets the authentication entry to be displayed to the user. */ + @NonNull + public Builder setAuthenticationEntry(@Nullable Entry authenticationEntry) { + mAuthenticationEntry = authenticationEntry; + return this; + } + + /** Sets the time when the provider was last used. */ + @NonNull + public Builder setLastUsedTimeMillis(@CurrentTimeMillisLong long lastUsedTimeMillis) { + mLastUsedTimeMillis = lastUsedTimeMillis; + return this; + } + + /** Builds a {@link ProviderData}. */ + @NonNull + public ProviderData build() { + return new ProviderData(mProviderId, mProviderDisplayName, mIcon, mCredentialEntries, + mActionChips, mAuthenticationEntry, mLastUsedTimeMillis); + } + } } diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java index 5de6d73945eb..eddb519051a9 100644 --- a/core/java/android/credentials/ui/RequestInfo.java +++ b/core/java/android/credentials/ui/RequestInfo.java @@ -36,12 +36,6 @@ public class RequestInfo implements Parcelable { */ public static final @NonNull String EXTRA_REQUEST_INFO = "android.credentials.ui.extra.REQUEST_INFO"; - /** - * The intent extra key for the {@code ResultReceiver} object when launching the UX - * activities. - */ - public static final @NonNull String EXTRA_RESULT_RECEIVER = - "android.credentials.ui.extra.RESULT_RECEIVER"; /** Type value for an executeGetCredential request. */ public static final @NonNull String TYPE_GET = "android.credentials.ui.TYPE_GET"; diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index dff2f7ed1cf3..50551feed522 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -133,9 +133,6 @@ public final class CameraManager { private HandlerThread mHandlerThread; private Handler mHandler; private FoldStateListener mFoldStateListener; - @GuardedBy("mLock") - private ArrayList<WeakReference<DeviceStateListener>> mDeviceStateListeners = new ArrayList<>(); - private boolean mFoldedDeviceState; /** * @hide @@ -144,31 +141,39 @@ public final class CameraManager { void onDeviceStateChanged(boolean folded); } - private final class FoldStateListener implements DeviceStateManager.DeviceStateCallback { + private static final class FoldStateListener implements DeviceStateManager.DeviceStateCallback { private final int[] mFoldedDeviceStates; + private ArrayList<WeakReference<DeviceStateListener>> mDeviceStateListeners = + new ArrayList<>(); + private boolean mFoldedDeviceState; + public FoldStateListener(Context context) { mFoldedDeviceStates = context.getResources().getIntArray( com.android.internal.R.array.config_foldedDeviceStates); } - private void handleStateChange(int state) { + private synchronized void handleStateChange(int state) { boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state); - synchronized (mLock) { - mFoldedDeviceState = folded; - ArrayList<WeakReference<DeviceStateListener>> invalidListeners = new ArrayList<>(); - for (WeakReference<DeviceStateListener> listener : mDeviceStateListeners) { - DeviceStateListener callback = listener.get(); - if (callback != null) { - callback.onDeviceStateChanged(folded); - } else { - invalidListeners.add(listener); - } - } - if (!invalidListeners.isEmpty()) { - mDeviceStateListeners.removeAll(invalidListeners); + + mFoldedDeviceState = folded; + ArrayList<WeakReference<DeviceStateListener>> invalidListeners = new ArrayList<>(); + for (WeakReference<DeviceStateListener> listener : mDeviceStateListeners) { + DeviceStateListener callback = listener.get(); + if (callback != null) { + callback.onDeviceStateChanged(folded); + } else { + invalidListeners.add(listener); } } + if (!invalidListeners.isEmpty()) { + mDeviceStateListeners.removeAll(invalidListeners); + } + } + + public synchronized void addDeviceStateListener(DeviceStateListener listener) { + listener.onDeviceStateChanged(mFoldedDeviceState); + mDeviceStateListeners.add(new WeakReference<>(listener)); } @Override @@ -192,9 +197,8 @@ public final class CameraManager { public void registerDeviceStateListener(@NonNull CameraCharacteristics chars) { synchronized (mLock) { DeviceStateListener listener = chars.getDeviceStateListener(); - listener.onDeviceStateChanged(mFoldedDeviceState); if (mFoldStateListener != null) { - mDeviceStateListeners.add(new WeakReference<>(listener)); + mFoldStateListener.addDeviceStateListener(listener); } } } diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java index 7247ef77afb4..197739b6a067 100644 --- a/core/java/android/hardware/face/FaceManager.java +++ b/core/java/android/hardware/face/FaceManager.java @@ -768,6 +768,20 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan } } + /** + * Schedules a watchdog. + * + * @hide + */ + @RequiresPermission(USE_BIOMETRIC_INTERNAL) + public void scheduleWatchdog() { + try { + mService.scheduleWatchdog(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + private void cancelEnrollment(long requestId) { if (mService != null) { try { diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl index 9b56f43a0f22..2bf187ac9006 100644 --- a/core/java/android/hardware/face/IFaceService.aidl +++ b/core/java/android/hardware/face/IFaceService.aidl @@ -172,4 +172,9 @@ interface IFaceService { // Registers BiometricStateListener. void registerBiometricStateListener(IBiometricStateListener listener); + + // Internal operation used to clear face biometric scheduler. + // Ensures that the scheduler is not stuck. + @EnforcePermission("USE_BIOMETRIC_INTERNAL") + void scheduleWatchdog(); } diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index 715361eafde9..5403f089b308 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -1125,6 +1125,20 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing } /** + * Schedules a watchdog. + * + * @hide + */ + @RequiresPermission(USE_BIOMETRIC_INTERNAL) + public void scheduleWatchdog() { + try { + mService.scheduleWatchdog(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * @hide */ public void addLockoutResetCallback(final LockoutResetCallback callback) { diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl index 1ba9a0471c88..051e3a4caa4e 100644 --- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl +++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl @@ -208,4 +208,9 @@ interface IFingerprintService { // Sends a power button pressed event to all listeners. @EnforcePermission("USE_BIOMETRIC_INTERNAL") oneway void onPowerPressed(); + + // Internal operation used to clear fingerprint biometric scheduler. + // Ensures that the scheduler is not stuck. + @EnforcePermission("USE_BIOMETRIC_INTERNAL") + void scheduleWatchdog(); } diff --git a/core/java/android/hardware/input/VirtualDpad.java b/core/java/android/hardware/input/VirtualDpad.java index d7cda9ec33cf..4d61553ccb52 100644 --- a/core/java/android/hardware/input/VirtualDpad.java +++ b/core/java/android/hardware/input/VirtualDpad.java @@ -24,7 +24,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.view.KeyEvent; -import java.io.Closeable; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -39,7 +38,7 @@ import java.util.Set; * @hide */ @SystemApi -public class VirtualDpad implements Closeable { +public class VirtualDpad extends VirtualInputDevice { private final Set<Integer> mSupportedKeyCodes = Collections.unmodifiableSet( @@ -50,23 +49,10 @@ public class VirtualDpad implements Closeable { KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_DPAD_CENTER))); - private final IVirtualDevice mVirtualDevice; - private final IBinder mToken; /** @hide */ public VirtualDpad(IVirtualDevice virtualDevice, IBinder token) { - mVirtualDevice = virtualDevice; - mToken = token; - } - - @Override - @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) - public void close() { - try { - mVirtualDevice.unregisterInputDevice(mToken); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + super(virtualDevice, token); } /** diff --git a/core/java/android/hardware/input/VirtualInputDevice.java b/core/java/android/hardware/input/VirtualInputDevice.java new file mode 100644 index 000000000000..772ba8e36c5e --- /dev/null +++ b/core/java/android/hardware/input/VirtualInputDevice.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input; + +import android.annotation.RequiresPermission; +import android.companion.virtual.IVirtualDevice; +import android.os.IBinder; +import android.os.RemoteException; + +import java.io.Closeable; + +/** + * The base class for all virtual input devices such as VirtualKeyboard, VirtualMouse. + * This implements the shared functionality such as closing the device and keeping track of + * identifiers. + * + * @hide + */ +abstract class VirtualInputDevice implements Closeable { + + /** + * The virtual device to which this VirtualInputDevice belongs to. + */ + protected final IVirtualDevice mVirtualDevice; + + /** + * The token used to uniquely identify the virtual input device. + */ + protected final IBinder mToken; + + /** @hide */ + VirtualInputDevice( + IVirtualDevice virtualDevice, IBinder token) { + mVirtualDevice = virtualDevice; + mToken = token; + } + + /** + * @return The device id of this device. + * @hide + */ + public int getInputDeviceId() { + try { + return mVirtualDevice.getInputDeviceId(mToken); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void close() { + try { + mVirtualDevice.unregisterInputDevice(mToken); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/hardware/input/VirtualKeyboard.java b/core/java/android/hardware/input/VirtualKeyboard.java index 901401fea32c..e569dbf6b6b6 100644 --- a/core/java/android/hardware/input/VirtualKeyboard.java +++ b/core/java/android/hardware/input/VirtualKeyboard.java @@ -24,8 +24,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.view.KeyEvent; -import java.io.Closeable; - /** * A virtual keyboard representing a key input mechanism on a remote device, such as a built-in * keyboard on a laptop, a software keyboard on a tablet, or a keypad on a TV remote control. @@ -36,26 +34,13 @@ import java.io.Closeable; * @hide */ @SystemApi -public class VirtualKeyboard implements Closeable { +public class VirtualKeyboard extends VirtualInputDevice { private final int mUnsupportedKeyCode = KeyEvent.KEYCODE_DPAD_CENTER; - private final IVirtualDevice mVirtualDevice; - private final IBinder mToken; /** @hide */ public VirtualKeyboard(IVirtualDevice virtualDevice, IBinder token) { - mVirtualDevice = virtualDevice; - mToken = token; - } - - @Override - @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) - public void close() { - try { - mVirtualDevice.unregisterInputDevice(mToken); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + super(virtualDevice, token); } /** diff --git a/core/java/android/hardware/input/VirtualMouse.java b/core/java/android/hardware/input/VirtualMouse.java index 6e2b56a2b5bc..7eba2b8bfdf0 100644 --- a/core/java/android/hardware/input/VirtualMouse.java +++ b/core/java/android/hardware/input/VirtualMouse.java @@ -25,8 +25,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.view.MotionEvent; -import java.io.Closeable; - /** * A virtual mouse representing a relative input mechanism on a remote device, such as a mouse or * trackpad. @@ -37,25 +35,11 @@ import java.io.Closeable; * @hide */ @SystemApi -public class VirtualMouse implements Closeable { - - private final IVirtualDevice mVirtualDevice; - private final IBinder mToken; +public class VirtualMouse extends VirtualInputDevice { /** @hide */ public VirtualMouse(IVirtualDevice virtualDevice, IBinder token) { - mVirtualDevice = virtualDevice; - mToken = token; - } - - @Override - @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) - public void close() { - try { - mVirtualDevice.unregisterInputDevice(mToken); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + super(virtualDevice, token); } /** diff --git a/core/java/android/hardware/input/VirtualTouchscreen.java b/core/java/android/hardware/input/VirtualTouchscreen.java index c8d602acaff6..0d07753b9b60 100644 --- a/core/java/android/hardware/input/VirtualTouchscreen.java +++ b/core/java/android/hardware/input/VirtualTouchscreen.java @@ -23,8 +23,6 @@ import android.companion.virtual.IVirtualDevice; import android.os.IBinder; import android.os.RemoteException; -import java.io.Closeable; - /** * A virtual touchscreen representing a touch-based display input mechanism on a remote device. * @@ -34,25 +32,10 @@ import java.io.Closeable; * @hide */ @SystemApi -public class VirtualTouchscreen implements Closeable { - - private final IVirtualDevice mVirtualDevice; - private final IBinder mToken; - +public class VirtualTouchscreen extends VirtualInputDevice { /** @hide */ public VirtualTouchscreen(IVirtualDevice virtualDevice, IBinder token) { - mVirtualDevice = virtualDevice; - mToken = token; - } - - @Override - @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) - public void close() { - try { - mVirtualDevice.unregisterInputDevice(mToken); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + super(virtualDevice, token); } /** diff --git a/core/java/android/hardware/radio/Announcement.java b/core/java/android/hardware/radio/Announcement.java index 8febed3fb2a0..3ba3ebceeb18 100644 --- a/core/java/android/hardware/radio/Announcement.java +++ b/core/java/android/hardware/radio/Announcement.java @@ -85,9 +85,9 @@ public final class Announcement implements Parcelable { /** @hide */ public Announcement(@NonNull ProgramSelector selector, @Type int type, @NonNull Map<String, String> vendorInfo) { - mSelector = Objects.requireNonNull(selector); - mType = Objects.requireNonNull(type); - mVendorInfo = Objects.requireNonNull(vendorInfo); + mSelector = Objects.requireNonNull(selector, "Program selector cannot be null"); + mType = type; + mVendorInfo = Objects.requireNonNull(vendorInfo, "Vendor info cannot be null"); } private Announcement(@NonNull Parcel in) { diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java index f2525d17e30a..ade9fd68ade8 100644 --- a/core/java/android/hardware/radio/ProgramList.java +++ b/core/java/android/hardware/radio/ProgramList.java @@ -160,6 +160,7 @@ public final class ProgramList implements AutoCloseable { * Disables list updates and releases all resources. */ public void close() { + OnCloseListener onCompleteListenersCopied = null; synchronized (mLock) { if (mIsClosed) return; mIsClosed = true; @@ -167,10 +168,14 @@ public final class ProgramList implements AutoCloseable { mListCallbacks.clear(); mOnCompleteListeners.clear(); if (mOnCloseListener != null) { - mOnCloseListener.onClose(); + onCompleteListenersCopied = mOnCloseListener; mOnCloseListener = null; } } + + if (onCompleteListenersCopied != null) { + onCompleteListenersCopied.onClose(); + } } void apply(Chunk chunk) { diff --git a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java index 891da2466bff..4f09beec81dd 100644 --- a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java +++ b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java @@ -777,7 +777,7 @@ final class IRemoteInputConnectionInvoker { } /** - * Invokes {@link IRemoteInputConnection#replaceText(InputConnectionCommandHeader, int, int, + * Invokes {@code IRemoteInputConnection#replaceText(InputConnectionCommandHeader, int, int, * CharSequence, TextAttribute)}. * * @param start the character index where the replacement should start. @@ -788,6 +788,8 @@ final class IRemoteInputConnectionInvoker { * that this means you can't position the cursor within the text. * @param text the text to replace. This may include styles. * @param textAttribute The extra information about the text. This value may be null. + * @return {@code true} if the invocation is completed without {@link RemoteException}, {@code + * false} otherwise. */ @AnyThread public boolean replaceText( diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index 4df013949de5..d3a6323230a5 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -1246,8 +1246,21 @@ public class Binder implements IBinder { // If the call was {@link IBinder#FLAG_ONEWAY} then these exceptions // disappear into the ether. final boolean tagEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_AIDL); + final boolean hasFullyQualifiedName = getMaxTransactionId() > 0; final String transactionTraceName; - if (tagEnabled) { + + if (tagEnabled && hasFullyQualifiedName) { + // If tracing enabled and we have a fully qualified name, fetch the name + transactionTraceName = getTransactionTraceName(code); + } else if (tagEnabled && isStackTrackingEnabled()) { + // If tracing is enabled and we *don't* have a fully qualified name, fetch the + // 'best effort' name only for stack tracking. This works around noticeable perf impact + // on low latency binder calls (<100us). The tracing call itself is between (1-10us) and + // the perf impact can be quite noticeable while benchmarking such binder calls. + // The primary culprits are ContentProviders and Cursors which convenienty don't + // autogenerate their AIDL and hence will not have a fully qualified name. + // + // TODO(b/253426478): Relax this constraint after a more robust fix transactionTraceName = getTransactionTraceName(code); } else { transactionTraceName = null; diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java index f62cc879cce3..8afd6de235a0 100644 --- a/core/java/android/os/PowerManagerInternal.java +++ b/core/java/android/os/PowerManagerInternal.java @@ -341,4 +341,10 @@ public abstract class PowerManagerInternal { * device is not awake. */ public abstract void nap(long eventTime, boolean allowWake); + + /** + * Returns true if ambient display is suppressed by any app with any token. This method will + * return false if ambient display is not available. + */ + public abstract boolean isAmbientDisplaySuppressed(); } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 4e15b38463d6..29e24598f874 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6875,6 +6875,14 @@ public final class Settings { @Readable public static final String VOICE_INTERACTION_SERVICE = "voice_interaction_service"; + + /** + * The currently selected credential service(s) flattened ComponentName. + * + * @hide + */ + public static final String CREDENTIAL_SERVICE = "credential_service"; + /** * The currently selected autofill service flattened ComponentName. * @hide diff --git a/core/java/android/service/credentials/CredentialProviderInfo.java b/core/java/android/service/credentials/CredentialProviderInfo.java new file mode 100644 index 000000000000..e3f8cb7bb23e --- /dev/null +++ b/core/java/android/service/credentials/CredentialProviderInfo.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.credentials; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.AppGlobals; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.os.RemoteException; +import android.util.Log; +import android.util.Slog; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * {@link ServiceInfo} and meta-data about a credential provider. + * + * @hide + */ +public final class CredentialProviderInfo { + private static final String TAG = "CredentialProviderInfo"; + + @NonNull + private final ServiceInfo mServiceInfo; + @NonNull + private final List<String> mCapabilities; + + // TODO: Move the two strings below to CredentialProviderService when ready. + private static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities"; + private static final String SERVICE_INTERFACE = + "android.service.credentials.CredentialProviderService"; + + + /** + * Constructs an information instance of the credential provider. + * + * @param context The context object + * @param serviceComponent The serviceComponent of the provider service + * @param userId The android userId for which the current process is running + * @throws PackageManager.NameNotFoundException If provider service is not found + */ + public CredentialProviderInfo(@NonNull Context context, + @NonNull ComponentName serviceComponent, int userId) + throws PackageManager.NameNotFoundException { + this(context, getServiceInfoOrThrow(serviceComponent, userId)); + } + + private CredentialProviderInfo(@NonNull Context context, @NonNull ServiceInfo serviceInfo) { + if (!Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE.equals(serviceInfo.permission)) { + Log.i(TAG, "Credential Provider Service from : " + serviceInfo.packageName + + "does not require permission" + + Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE); + throw new SecurityException("Service does not require the expected permission : " + + Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE); + } + mServiceInfo = serviceInfo; + mCapabilities = new ArrayList<>(); + populateProviderCapabilities(context); + } + + private void populateProviderCapabilities(@NonNull Context context) { + if (mServiceInfo.applicationInfo.metaData == null) { + return; + } + try { + final int resourceId = mServiceInfo.applicationInfo.metaData.getInt( + CAPABILITY_META_DATA_KEY); + String[] capabilities = context.getResources().getStringArray(resourceId); + if (capabilities == null) { + Log.w(TAG, "No capabilities found for provider: " + mServiceInfo.packageName); + return; + } + for (String capability : capabilities) { + if (capability.isEmpty()) { + Log.w(TAG, "Skipping empty capability"); + continue; + } + mCapabilities.add(capability); + } + } catch (Resources.NotFoundException e) { + Log.w(TAG, "Exception while populating provider capabilities: " + e.getMessage()); + } + } + + private static ServiceInfo getServiceInfoOrThrow(@NonNull ComponentName serviceComponent, + int userId) throws PackageManager.NameNotFoundException { + try { + ServiceInfo si = AppGlobals.getPackageManager().getServiceInfo( + serviceComponent, + PackageManager.GET_META_DATA, + userId); + if (si != null) { + return si; + } + } catch (RemoteException e) { + Slog.v(TAG, e.getMessage()); + } + throw new PackageManager.NameNotFoundException(serviceComponent.toString()); + } + + /** + * Returns true if the service supports the given {@code credentialType}, false otherwise. + */ + @NonNull + public boolean hasCapability(@NonNull String credentialType) { + return mCapabilities.contains(credentialType); + } + + /** Returns the service info. */ + @NonNull + public ServiceInfo getServiceInfo() { + return mServiceInfo; + } + + /** Returns an immutable list of capabilities this provider service can support. */ + @NonNull + public List<String> getCapabilities() { + return Collections.unmodifiableList(mCapabilities); + } + + /** + * Returns the valid credential provider services available for the user with the + * given {@code userId}. + */ + public static List<CredentialProviderInfo> getAvailableServices(@NonNull Context context, + @UserIdInt int userId) { + final List<CredentialProviderInfo> services = new ArrayList<>(); + + final List<ResolveInfo> resolveInfos = + context.getPackageManager().queryIntentServicesAsUser( + new Intent(SERVICE_INTERFACE), + PackageManager.GET_META_DATA, + userId); + for (ResolveInfo resolveInfo : resolveInfos) { + final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + try { + services.add(new CredentialProviderInfo(context, serviceInfo)); + } catch (SecurityException e) { + Log.w(TAG, "Error getting info for " + serviceInfo + ": " + e); + } + } + return services; + } + + /** + * Returns the valid credential provider services available for the user, that can + * support the given {@code credentialType}. + */ + public static List<CredentialProviderInfo> getAvailableServicesForCapability( + Context context, @UserIdInt int userId, String credentialType) { + List<CredentialProviderInfo> servicesForCapability = new ArrayList<>(); + final List<CredentialProviderInfo> services = getAvailableServices(context, userId); + + for (CredentialProviderInfo service : services) { + if (service.hasCapability(credentialType)) { + servicesForCapability.add(service); + } + } + return servicesForCapability; + } +} diff --git a/core/java/android/service/dreams/DreamActivity.java b/core/java/android/service/dreams/DreamActivity.java index f6a7c8eb8c4b..a2fa1392b079 100644 --- a/core/java/android/service/dreams/DreamActivity.java +++ b/core/java/android/service/dreams/DreamActivity.java @@ -44,6 +44,8 @@ import android.text.TextUtils; public class DreamActivity extends Activity { static final String EXTRA_CALLBACK = "binder"; static final String EXTRA_DREAM_TITLE = "title"; + @Nullable + private DreamService.DreamActivityCallbacks mCallback; public DreamActivity() {} @@ -57,11 +59,19 @@ public class DreamActivity extends Activity { } final Bundle extras = getIntent().getExtras(); - final DreamService.DreamActivityCallback callback = - (DreamService.DreamActivityCallback) extras.getBinder(EXTRA_CALLBACK); + mCallback = (DreamService.DreamActivityCallbacks) extras.getBinder(EXTRA_CALLBACK); - if (callback != null) { - callback.onActivityCreated(this); + if (mCallback != null) { + mCallback.onActivityCreated(this); } } + + @Override + public void onDestroy() { + if (mCallback != null) { + mCallback.onActivityDestroyed(); + } + + super.onDestroy(); + } } diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 3c1fef02f9ba..cb0dce91589e 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -1295,7 +1295,7 @@ public class DreamService extends Service implements Window.Callback { Intent i = new Intent(this, DreamActivity.class); i.setPackage(getApplicationContext().getPackageName()); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallback(mDreamToken)); + i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallbacks(mDreamToken)); final ServiceInfo serviceInfo = fetchServiceInfo(this, new ComponentName(this, getClass())); i.putExtra(DreamActivity.EXTRA_DREAM_TITLE, fetchDreamLabel(this, serviceInfo)); @@ -1488,10 +1488,10 @@ public class DreamService extends Service implements Window.Callback { } /** @hide */ - final class DreamActivityCallback extends Binder { + final class DreamActivityCallbacks extends Binder { private final IBinder mActivityDreamToken; - DreamActivityCallback(IBinder token) { + DreamActivityCallbacks(IBinder token) { mActivityDreamToken = token; } @@ -1516,6 +1516,12 @@ public class DreamService extends Service implements Window.Callback { mActivity = activity; onWindowCreated(activity.getWindow()); } + + // If DreamActivity is destroyed, wake up from Dream. + void onActivityDestroyed() { + mActivity = null; + onDestroy(); + } } /** diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index bd4a4957775e..cfc79e4fef66 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -392,13 +392,13 @@ public abstract class NotificationListenerService extends Service { public static final int NOTIFICATION_CHANNEL_OR_GROUP_DELETED = 3; /** - * An optional activity intent category that shows additional settings for what notifications + * An optional activity intent action that shows additional settings for what notifications * should be processed by this notification listener service. If defined, the OS may link to * this activity from the system notification listener service filter settings page. */ - @SdkConstant(SdkConstant.SdkConstantType.INTENT_CATEGORY) - public static final String INTENT_CATEGORY_SETTINGS_HOME = - "android.service.notification.category.SETTINGS_HOME"; + @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SETTINGS_HOME = + "android.service.notification.action.SETTINGS_HOME"; private final Object mLock = new Object(); diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index b559161ae0ad..a59d429b24e6 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -284,7 +284,6 @@ public abstract class WallpaperService extends Service { private Display mDisplay; private Context mDisplayContext; private int mDisplayState; - private @Surface.Rotation int mDisplayInstallOrientation; private float mWallpaperDimAmount = 0.05f; private float mPreviousWallpaperDimAmount = mWallpaperDimAmount; private float mDefaultDimAmount = mWallpaperDimAmount; @@ -1159,7 +1158,7 @@ public abstract class WallpaperService extends Service { mSurfaceControl, mInsetsState, mTempControls, mSyncSeqIdBundle); final int transformHint = SurfaceControl.rotationToBufferTransform( - (mDisplayInstallOrientation + mDisplay.getRotation()) % 4); + (mDisplay.getInstallOrientation() + mDisplay.getRotation()) % 4); mSurfaceControl.setTransformHint(transformHint); WindowLayout.computeSurfaceSize(mLayout, maxBounds, mWidth, mHeight, mWinFrames.frame, false /* dragResizing */, mSurfaceSize); @@ -1420,7 +1419,6 @@ public abstract class WallpaperService extends Service { mWallpaperDimAmount = mDefaultDimAmount; mPreviousWallpaperDimAmount = mWallpaperDimAmount; mDisplayState = mDisplay.getState(); - mDisplayInstallOrientation = mDisplay.getInstallOrientation(); if (DEBUG) Log.v(TAG, "onCreate(): " + this); onCreate(mSurfaceHolder); diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 519fc55b523d..1337d6a87df8 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -36,7 +36,6 @@ import android.text.style.LineBackgroundSpan; import android.text.style.ParagraphStyle; import android.text.style.ReplacementSpan; import android.text.style.TabStopSpan; -import android.util.Range; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; @@ -1859,13 +1858,12 @@ public abstract class Layout { * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a * text segment * @param inclusionStrategy strategy for determining whether a text segment is inside the - * specified area - * @return an integer range where the endpoints are the start (inclusive) and end (exclusive) - * character offsets of the text range, or null if there are no text segments inside the - * area + * specified area + * @return int array of size 2 containing the start (inclusive) and end (exclusive) character + * offsets of the text range, or null if there are no text segments inside the area */ @Nullable - public Range<Integer> getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder, + public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull TextInclusionStrategy inclusionStrategy) { // Find the first line whose bottom (without line spacing) is below the top of the area. int startLine = getLineForVertical((int) area.top); @@ -1923,7 +1921,7 @@ public abstract class Layout { start = segmentFinder.previousStartBoundary(start + 1); end = segmentFinder.nextEndBoundary(end - 1); - return new Range(start, end); + return new int[] {start, end}; } /** diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index a1ece9298e7f..a0a07b30662f 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -162,7 +162,13 @@ public class HandwritingInitiator { if (candidateView == getConnectedView()) { startHandwriting(candidateView); } else { - candidateView.requestFocus(); + if (candidateView.getRevealOnFocusHint()) { + candidateView.setRevealOnFocusHint(false); + candidateView.requestFocus(); + candidateView.setRevealOnFocusHint(true); + } else { + candidateView.requestFocus(); + } } } } diff --git a/core/java/android/view/ImeFocusController.java b/core/java/android/view/ImeFocusController.java index 4de7c4fdd513..43828d58afc4 100644 --- a/core/java/android/view/ImeFocusController.java +++ b/core/java/android/view/ImeFocusController.java @@ -108,10 +108,11 @@ public final class ImeFocusController { } /** - * @see InputMethodManager#checkFocus() + * @see ViewRootImpl#dispatchCheckFocus() */ - public boolean checkFocus(boolean forceNewFocus, boolean startInput) { - return getImmDelegate().checkFocus(forceNewFocus, startInput, mViewRootImpl); + @UiThread + void onScheduledCheckFocus() { + getImmDelegate().onScheduledCheckFocus(mViewRootImpl); } @UiThread @@ -163,7 +164,7 @@ public final class ImeFocusController { void onPostWindowGainedFocus(View viewForWindowFocus, @NonNull WindowManager.LayoutParams windowAttribute); void onViewFocusChanged(@NonNull View view, boolean hasFocus); - boolean checkFocus(boolean forceNewFocus, boolean startInput, ViewRootImpl viewRootImpl); + void onScheduledCheckFocus(@NonNull ViewRootImpl viewRootImpl); void onViewDetachedFromWindow(View view, ViewRootImpl viewRootImpl); void onWindowDismissed(ViewRootImpl viewRootImpl); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 58c81260076b..efda257aed27 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -428,8 +428,6 @@ public final class ViewRootImpl implements ViewParent, final DisplayManager mDisplayManager; final String mBasePackageName; - private @Surface.Rotation int mDisplayInstallOrientation; - final int[] mTmpLocation = new int[2]; final TypedValue mTmpValue = new TypedValue(); @@ -890,20 +888,18 @@ public final class ViewRootImpl implements ViewParent, static BLASTBufferQueue.TransactionHangCallback sTransactionHangCallback = new BLASTBufferQueue.TransactionHangCallback() { @Override - public void onTransactionHang(boolean isGPUHang) { - if (isGPUHang && !sAnrReported) { - sAnrReported = true; - try { - ActivityManager.getService().appNotResponding( - "Buffer processing hung up due to stuck fence. Indicates GPU hang"); - } catch (RemoteException e) { - // We asked the system to crash us, but the system - // already crashed. Unfortunately things may be - // out of control. - } - } else { - // TODO: Do something with this later. For now we just ANR - // in dequeue buffer later like we always have. + public void onTransactionHang(String reason) { + if (sAnrReported) { + return; + } + + sAnrReported = true; + try { + ActivityManager.getService().appNotResponding(reason); + } catch (RemoteException e) { + // We asked the system to crash us, but the system + // already crashed. Unfortunately things may be + // out of control. } } }; @@ -1134,7 +1130,6 @@ public final class ViewRootImpl implements ViewParent, if (mView == null) { mView = view; - mDisplayInstallOrientation = mDisplay.getInstallOrientation(); mViewLayoutDirectionInitial = mView.getRawLayoutDirection(); mFallbackEventHandler.setView(view); mWindowAttributes.copyFrom(attrs); @@ -1905,7 +1900,6 @@ public final class ViewRootImpl implements ViewParent, updateInternalDisplay(displayId, mView.getResources()); mImeFocusController.onMovedToDisplay(); mAttachInfo.mDisplayState = mDisplay.getState(); - mDisplayInstallOrientation = mDisplay.getInstallOrientation(); // Internal state updated, now notify the view hierarchy. mView.dispatchMovedToDisplay(mDisplay, config); } @@ -5718,7 +5712,7 @@ public final class ViewRootImpl implements ViewParent, enqueueInputEvent(event, null, 0, true); } break; case MSG_CHECK_FOCUS: { - getImeFocusController().checkFocus(false, true); + getImeFocusController().onScheduledCheckFocus(); } break; case MSG_CLOSE_SYSTEM_DIALOGS: { if (mView != null) { @@ -8235,7 +8229,7 @@ public final class ViewRootImpl implements ViewParent, } final int transformHint = SurfaceControl.rotationToBufferTransform( - (mDisplayInstallOrientation + mDisplay.getRotation()) % 4); + (mDisplay.getInstallOrientation() + mDisplay.getRotation()) % 4); WindowLayout.computeSurfaceSize(mWindowAttributes, winConfig.getMaxBounds(), requestedWidth, requestedHeight, mWinFrameInScreen, mPendingDragResizing, mSurfaceSize); @@ -8260,7 +8254,7 @@ public final class ViewRootImpl implements ViewParent, } mLastTransformHint = transformHint; - + mSurfaceControl.setTransformHint(transformHint); if (mAttachInfo.mContentCaptureManager != null) { diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java index 4a79ba62de69..febdac26a0f2 100644 --- a/core/java/android/view/inputmethod/EditorInfo.java +++ b/core/java/android/view/inputmethod/EditorInfo.java @@ -595,6 +595,10 @@ public class EditorInfo implements InputType, Parcelable { == HandwritingGesture.GESTURE_TYPE_SELECT) { list.add(SelectGesture.class); } + if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_SELECT_RANGE) + == HandwritingGesture.GESTURE_TYPE_SELECT_RANGE) { + list.add(SelectRangeGesture.class); + } if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_INSERT) == HandwritingGesture.GESTURE_TYPE_INSERT) { list.add(InsertGesture.class); @@ -603,6 +607,10 @@ public class EditorInfo implements InputType, Parcelable { == HandwritingGesture.GESTURE_TYPE_DELETE) { list.add(DeleteGesture.class); } + if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_DELETE_RANGE) + == HandwritingGesture.GESTURE_TYPE_DELETE_RANGE) { + list.add(DeleteRangeGesture.class); + } if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_REMOVE_SPACE) == HandwritingGesture.GESTURE_TYPE_REMOVE_SPACE) { list.add(RemoveSpaceGesture.class); diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 18b3e212d6f4..574d0356ae42 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -30,7 +30,9 @@ import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodCl import static android.view.inputmethod.InputMethodManagerProto.ACTIVE; import static android.view.inputmethod.InputMethodManagerProto.CUR_ID; import static android.view.inputmethod.InputMethodManagerProto.FULLSCREEN_MODE; +import static android.view.inputmethod.InputMethodManagerProto.NEXT_SERVED_VIEW; import static android.view.inputmethod.InputMethodManagerProto.SERVED_CONNECTING; +import static android.view.inputmethod.InputMethodManagerProto.SERVED_VIEW; import static com.android.internal.inputmethod.StartInputReason.BOUND_TO_IMMS; @@ -763,39 +765,37 @@ public final class InputMethodManager { forceFocus = true; } } - startInputOnWindowFocusGain(viewForWindowFocus, - windowAttribute.softInputMode, windowAttribute.flags, forceFocus); - } - private void startInputOnWindowFocusGain(View focusedView, - @SoftInputModeFlags int softInputMode, int windowFlags, boolean forceNewFocus) { - int startInputFlags = getStartInputFlags(focusedView, 0); + final int softInputMode = windowAttribute.softInputMode; + final int windowFlags = windowAttribute.flags; + + int startInputFlags = getStartInputFlags(viewForWindowFocus, 0); startInputFlags |= StartInputFlags.WINDOW_GAINED_FOCUS; ImeTracing.getInstance().triggerClientDump( "InputMethodManager.DelegateImpl#startInputAsyncOnWindowFocusGain", InputMethodManager.this, null /* icProto */); - final ImeFocusController controller = getFocusController(); - if (controller == null) { - return; - } - + boolean checkFocusResult; synchronized (mH) { + if (mCurRootView == null) { + return; + } if (mRestartOnNextWindowFocus) { if (DEBUG) Log.v(TAG, "Restarting due to mRestartOnNextWindowFocus as true"); mRestartOnNextWindowFocus = false; - forceNewFocus = true; + forceFocus = true; } + checkFocusResult = checkFocusInternalLocked(forceFocus, mCurRootView); } - if (controller.checkFocus(forceNewFocus, false)) { + if (checkFocusResult) { // We need to restart input on the current focus view. This // should be done in conjunction with telling the system service // about the window gaining focus, to help make the transition // smooth. if (startInputOnWindowFocusGainInternal(StartInputReason.WINDOW_FOCUS_GAIN, - focusedView, startInputFlags, softInputMode, windowFlags)) { + viewForWindowFocus, startInputFlags, softInputMode, windowFlags)) { return; } } @@ -810,7 +810,7 @@ public final class InputMethodManager { // ignore the result mServiceInvoker.startInputOrWindowGainedFocus( StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY, mClient, - focusedView.getWindowToken(), startInputFlags, softInputMode, + viewForWindowFocus.getWindowToken(), startInputFlags, softInputMode, windowFlags, null, null, null, @@ -825,9 +825,15 @@ public final class InputMethodManager { } @Override - public boolean checkFocus(boolean forceNewFocus, boolean startInput, - ViewRootImpl viewRootImpl) { - return checkFocusInternal(forceNewFocus, startInput, viewRootImpl); + public void onScheduledCheckFocus(ViewRootImpl viewRootImpl) { + synchronized (mH) { + if (!checkFocusInternalLocked(false, viewRootImpl)) { + return; + } + } + startInputOnWindowFocusGainInternal(StartInputReason.SCHEDULED_CHECK_FOCUS, + null /* focusedView */, 0 /* startInputFlags */, 0 /* softInputMode */, + 0 /* windowFlags */); } @Override @@ -937,15 +943,6 @@ public final class InputMethodManager { return mCurRootView != null ? mNextServedView : null; } - private ImeFocusController getFocusController() { - synchronized (mH) { - if (mCurRootView != null) { - return mCurRootView.getImeFocusController(); - } - return null; - } - } - /** * Returns {@code true} when the given view has been served by Input Method. */ @@ -1128,8 +1125,7 @@ public final class InputMethodManager { if (mCurRootView == null) { return; } - if (!mCurRootView.getImeFocusController().checkFocus( - mRestartOnNextWindowFocus, false)) { + if (!checkFocusInternalLocked(mRestartOnNextWindowFocus, mCurRootView)) { return; } final int reason = active ? StartInputReason.ACTIVATED_BY_IMMS @@ -2349,8 +2345,7 @@ public final class InputMethodManager { } /** - * Called from {@link #checkFocusInternal(boolean, boolean, ViewRootImpl)}, - * {@link #restartInput(View)}, {@link #MSG_BIND} or {@link #MSG_UNBIND}. + * Starts an input connection from the served view that gains the window focus. * Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input * background thread may blocked by other methods which already inside {@code mH} lock. */ @@ -2658,52 +2653,53 @@ public final class InputMethodManager { } /** - * Check the next served view from {@link ImeFocusController} if needs to start input. * Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input * background thread may blocked by other methods which already inside {@code mH} lock. * @hide */ @UnsupportedAppUsage public void checkFocus() { - final ImeFocusController controller = getFocusController(); - if (controller != null) { - controller.checkFocus(false /* forceNewFocus */, true /* startInput */); - } - } - - private boolean checkFocusInternal(boolean forceNewFocus, boolean startInput, - ViewRootImpl viewRootImpl) { synchronized (mH) { - if (mCurRootView != viewRootImpl) { - return false; - } - if (mServedView == mNextServedView && !forceNewFocus) { - return false; - } - if (DEBUG) { - Log.v(TAG, "checkFocus: view=" + mServedView - + " next=" + mNextServedView - + " force=" + forceNewFocus - + " package=" - + (mServedView != null ? mServedView.getContext().getPackageName() - : "<none>")); - } - // Close the connection when no next served view coming. - if (mNextServedView == null) { - finishInputLocked(); - closeCurrentInput(); - return false; + if (mCurRootView == null) { + return; } - mServedView = mNextServedView; - if (mServedInputConnection != null) { - mServedInputConnection.finishComposingTextFromImm(); + if (!checkFocusInternalLocked(false /* forceNewFocus */, mCurRootView)) { + return; } } + startInputOnWindowFocusGainInternal(StartInputReason.CHECK_FOCUS, + null /* focusedView */, + 0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */); + } - if (startInput) { - startInputOnWindowFocusGainInternal(StartInputReason.CHECK_FOCUS, - null /* focusedView */, - 0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */); + /** + * Check the next served view if needs to start input. + */ + @GuardedBy("mH") + private boolean checkFocusInternalLocked(boolean forceNewFocus, ViewRootImpl viewRootImpl) { + if (mCurRootView != viewRootImpl) { + return false; + } + if (mServedView == mNextServedView && !forceNewFocus) { + return false; + } + if (DEBUG) { + Log.v(TAG, "checkFocus: view=" + mServedView + + " next=" + mNextServedView + + " force=" + forceNewFocus + + " package=" + + (mServedView != null ? mServedView.getContext().getPackageName() + : "<none>")); + } + // Close the connection when no next served view coming. + if (mNextServedView == null) { + finishInputLocked(); + closeCurrentInput(); + return false; + } + mServedView = mNextServedView; + if (mServedInputConnection != null) { + mServedInputConnection.finishComposingTextFromImm(); } return true; } @@ -3996,6 +3992,8 @@ public final class InputMethodManager { proto.write(FULLSCREEN_MODE, mFullscreenMode); proto.write(ACTIVE, mActive); proto.write(SERVED_CONNECTING, mServedConnecting); + proto.write(SERVED_VIEW, Objects.toString(mServedView)); + proto.write(NEXT_SERVED_VIEW, Objects.toString(mNextServedView)); proto.end(token); if (mCurRootView != null) { mCurRootView.dumpDebug(proto, VIEW_ROOT_IMPL); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index a2e9faa81eeb..b5c58fb4bfc0 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -151,7 +151,6 @@ import android.util.DisplayMetrics; import android.util.FeatureFlagUtils; import android.util.IntArray; import android.util.Log; -import android.util.Range; import android.util.SparseIntArray; import android.util.TypedValue; import android.view.AccessibilityIterators.TextSegmentIterator; @@ -189,6 +188,7 @@ import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.DeleteGesture; +import android.view.inputmethod.DeleteRangeGesture; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; @@ -199,6 +199,7 @@ import android.view.inputmethod.InsertGesture; import android.view.inputmethod.JoinOrSplitGesture; import android.view.inputmethod.RemoveSpaceGesture; import android.view.inputmethod.SelectGesture; +import android.view.inputmethod.SelectRangeGesture; import android.view.inspector.InspectableProperty; import android.view.inspector.InspectableProperty.EnumEntry; import android.view.inspector.InspectableProperty.FlagEntry; @@ -9096,7 +9097,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ArrayList<Class<? extends HandwritingGesture>> gestures = new ArrayList<>(); gestures.add(SelectGesture.class); + gestures.add(SelectRangeGesture.class); gestures.add(DeleteGesture.class); + gestures.add(DeleteRangeGesture.class); gestures.add(InsertGesture.class); gestures.add(RemoveSpaceGesture.class); gestures.add(JoinOrSplitGesture.class); @@ -9313,82 +9316,145 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** @hide */ public int performHandwritingSelectGesture(@NonNull SelectGesture gesture) { - Range<Integer> range = getRangeForRect( + int[] range = getRangeForRect( convertFromScreenToContentCoordinates(gesture.getSelectionArea()), gesture.getGranularity()); if (range == null) { return handleGestureFailure(gesture); } - Selection.setSelection(getEditableText(), range.getLower(), range.getUpper()); + Selection.setSelection(getEditableText(), range[0], range[1]); + mEditor.startSelectionActionModeAsync(/* adjustSelection= */ false); + return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS; + } + + /** @hide */ + public int performHandwritingSelectRangeGesture(@NonNull SelectRangeGesture gesture) { + int[] startRange = getRangeForRect( + convertFromScreenToContentCoordinates(gesture.getSelectionStartArea()), + gesture.getGranularity()); + if (startRange == null) { + return handleGestureFailure(gesture); + } + int[] endRange = getRangeForRect( + convertFromScreenToContentCoordinates(gesture.getSelectionEndArea()), + gesture.getGranularity()); + if (endRange == null) { + return handleGestureFailure(gesture); + } + int[] range = new int[] { + Math.min(startRange[0], endRange[0]), Math.max(startRange[1], endRange[1]) + }; + Selection.setSelection(getEditableText(), range[0], range[1]); mEditor.startSelectionActionModeAsync(/* adjustSelection= */ false); return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS; } /** @hide */ public int performHandwritingDeleteGesture(@NonNull DeleteGesture gesture) { - Range<Integer> range = getRangeForRect( + int[] range = getRangeForRect( convertFromScreenToContentCoordinates(gesture.getDeletionArea()), gesture.getGranularity()); if (range == null) { return handleGestureFailure(gesture); } - int start = range.getLower(); - int end = range.getUpper(); - // For word granularity, adjust the start and end offsets to remove extra whitespace around - // the deleted text. if (gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD) { - // If the deleted text is at the start of the text, the behavior is the same as the case - // where the deleted text follows a new line character. - int codePointBeforeStart = start > 0 - ? Character.codePointBefore(mText, start) : TextUtils.LINE_FEED_CODE_POINT; - // If the deleted text is at the end of the text, the behavior is the same as the case - // where the deleted text precedes a new line character. - int codePointAtEnd = end < mText.length() - ? Character.codePointAt(mText, end) : TextUtils.LINE_FEED_CODE_POINT; - if (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart) - && (TextUtils.isWhitespace(codePointAtEnd) - || TextUtils.isPunctuation(codePointAtEnd))) { - // Remove whitespace (except new lines) before the deleted text, in these cases: - // - There is whitespace following the deleted text - // e.g. "one [deleted] three" -> "one | three" -> "one| three" - // - There is punctuation following the deleted text - // e.g. "one [deleted]!" -> "one |!" -> "one|!" - // - There is a new line following the deleted text - // e.g. "one [deleted]\n" -> "one |\n" -> "one|\n" - // - The deleted text is at the end of the text - // e.g. "one [deleted]" -> "one |" -> "one|" - // (The pipe | indicates the cursor position.) - do { - start -= Character.charCount(codePointBeforeStart); - if (start == 0) break; - codePointBeforeStart = Character.codePointBefore(mText, start); - } while (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart)); - } else if (TextUtils.isWhitespaceExceptNewline(codePointAtEnd) - && (TextUtils.isWhitespace(codePointBeforeStart) - || TextUtils.isPunctuation(codePointBeforeStart))) { - // Remove whitespace (except new lines) after the deleted text, in these cases: - // - There is punctuation preceding the deleted text - // e.g. "([deleted] two)" -> "(| two)" -> "(|two)" - // - There is a new line preceding the deleted text - // e.g. "\n[deleted] two" -> "\n| two" -> "\n|two" - // - The deleted text is at the start of the text - // e.g. "[deleted] two" -> "| two" -> "|two" - // (The pipe | indicates the cursor position.) - do { - end += Character.charCount(codePointAtEnd); - if (end == mText.length()) break; - codePointAtEnd = Character.codePointAt(mText, end); - } while (TextUtils.isWhitespaceExceptNewline(codePointAtEnd)); - } - } - - getEditableText().delete(start, end); - Selection.setSelection(getEditableText(), start); + range = adjustHandwritingDeleteGestureRange(range); + } + + getEditableText().delete(range[0], range[1]); + Selection.setSelection(getEditableText(), range[0]); return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS; } /** @hide */ + public int performHandwritingDeleteRangeGesture(@NonNull DeleteRangeGesture gesture) { + int[] startRange = getRangeForRect( + convertFromScreenToContentCoordinates(gesture.getDeletionStartArea()), + gesture.getGranularity()); + if (startRange == null) { + return handleGestureFailure(gesture); + } + int[] endRange = getRangeForRect( + convertFromScreenToContentCoordinates(gesture.getDeletionEndArea()), + gesture.getGranularity()); + if (endRange == null) { + return handleGestureFailure(gesture); + } + int[] range = new int[] { + Math.min(startRange[0], endRange[0]), Math.max(startRange[1], endRange[1]) + }; + + if (gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD) { + range = adjustHandwritingDeleteGestureRange(range); + } + + getEditableText().delete(range[0], range[1]); + Selection.setSelection(getEditableText(), range[0]); + return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS; + } + + private int[] adjustHandwritingDeleteGestureRange(int[] range) { + // For handwriting delete gestures with word granularity, adjust the start and end offsets + // to remove extra whitespace around the deleted text. + + int start = range[0]; + int end = range[1]; + + // If the deleted text is at the start of the text, the behavior is the same as the case + // where the deleted text follows a new line character. + int codePointBeforeStart = start > 0 + ? Character.codePointBefore(mText, start) : TextUtils.LINE_FEED_CODE_POINT; + // If the deleted text is at the end of the text, the behavior is the same as the case where + // the deleted text precedes a new line character. + int codePointAtEnd = end < mText.length() + ? Character.codePointAt(mText, end) : TextUtils.LINE_FEED_CODE_POINT; + + if (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart) + && (TextUtils.isWhitespace(codePointAtEnd) + || TextUtils.isPunctuation(codePointAtEnd))) { + // Remove whitespace (except new lines) before the deleted text, in these cases: + // - There is whitespace following the deleted text + // e.g. "one [deleted] three" -> "one | three" -> "one| three" + // - There is punctuation following the deleted text + // e.g. "one [deleted]!" -> "one |!" -> "one|!" + // - There is a new line following the deleted text + // e.g. "one [deleted]\n" -> "one |\n" -> "one|\n" + // - The deleted text is at the end of the text + // e.g. "one [deleted]" -> "one |" -> "one|" + // (The pipe | indicates the cursor position.) + do { + start -= Character.charCount(codePointBeforeStart); + if (start == 0) break; + codePointBeforeStart = Character.codePointBefore(mText, start); + } while (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart)); + return new int[] {start, end}; + } + + if (TextUtils.isWhitespaceExceptNewline(codePointAtEnd) + && (TextUtils.isWhitespace(codePointBeforeStart) + || TextUtils.isPunctuation(codePointBeforeStart))) { + // Remove whitespace (except new lines) after the deleted text, in these cases: + // - There is punctuation preceding the deleted text + // e.g. "([deleted] two)" -> "(| two)" -> "(|two)" + // - There is a new line preceding the deleted text + // e.g. "\n[deleted] two" -> "\n| two" -> "\n|two" + // - The deleted text is at the start of the text + // e.g. "[deleted] two" -> "| two" -> "|two" + // (The pipe | indicates the cursor position.) + do { + end += Character.charCount(codePointAtEnd); + if (end == mText.length()) break; + codePointAtEnd = Character.codePointAt(mText, end); + } while (TextUtils.isWhitespaceExceptNewline(codePointAtEnd)); + return new int[] {start, end}; + } + + // Return the original range. + return range; + } + + /** @hide */ public int performHandwritingInsertGesture(@NonNull InsertGesture gesture) { PointF point = convertFromScreenToContentCoordinates(gesture.getInsertionPoint()); int line = getLineForHandwritingGesture(point); @@ -9431,14 +9497,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener lineVerticalCenter + 0.1f, Math.max(startPoint.x, endPoint.x), lineVerticalCenter - 0.1f); - Range<Integer> range = mLayout.getRangeForRect( + int[] range = mLayout.getRangeForRect( area, new GraphemeClusterSegmentFinder(mText, mTextPaint), Layout.INCLUSION_STRATEGY_ANY_OVERLAP); if (range == null) { return handleGestureFailure(gesture); } - int startOffset = range.getLower(); - int endOffset = range.getUpper(); + int startOffset = range[0]; + int endOffset = range[1]; // TODO(b/247557062): This doesn't handle bidirectional text correctly. Pattern whitespacePattern = getWhitespacePattern(); @@ -9543,7 +9609,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Nullable - private Range<Integer> getRangeForRect(@NonNull RectF area, int granularity) { + private int[] getRangeForRect(@NonNull RectF area, int granularity) { SegmentFinder segmentFinder; if (granularity == HandwritingGesture.GRANULARITY_WORD) { WordIterator wordIterator = getWordIterator(); diff --git a/core/java/com/android/internal/backup/IBackupTransport.aidl b/core/java/com/android/internal/backup/IBackupTransport.aidl index f09e176beea0..21c7baab4e83 100644 --- a/core/java/com/android/internal/backup/IBackupTransport.aidl +++ b/core/java/com/android/internal/backup/IBackupTransport.aidl @@ -16,6 +16,7 @@ package com.android.internal.backup; +import android.app.backup.IBackupManagerMonitor; import android.app.backup.RestoreDescription; import android.app.backup.RestoreSet; import android.content.Intent; @@ -400,4 +401,13 @@ oneway interface IBackupTransport { * <p>For supported flags see {@link android.app.backup.BackupAgent}. */ void getTransportFlags(in AndroidFuture<int> resultFuture); + + /** + * Ask the transport for a {@link IBackupManagerMonitor} instance which will be used by the + * framework to report logging events back to the transport. + * + * Backups requested from outside the framework may pass in a monitor with the request, + * however backups initiated by the framework will call this method to retrieve one. + */ + void getBackupManagerMonitor(in AndroidFuture<IBackupManagerMonitor> resultFuture); } diff --git a/core/java/com/android/internal/inputmethod/EditableInputConnection.java b/core/java/com/android/internal/inputmethod/EditableInputConnection.java index f260d7dfc6a6..f600c36cd8c9 100644 --- a/core/java/com/android/internal/inputmethod/EditableInputConnection.java +++ b/core/java/com/android/internal/inputmethod/EditableInputConnection.java @@ -35,6 +35,7 @@ import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.DeleteGesture; +import android.view.inputmethod.DeleteRangeGesture; import android.view.inputmethod.DumpableInputConnection; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; @@ -44,6 +45,7 @@ import android.view.inputmethod.InsertGesture; import android.view.inputmethod.JoinOrSplitGesture; import android.view.inputmethod.RemoveSpaceGesture; import android.view.inputmethod.SelectGesture; +import android.view.inputmethod.SelectRangeGesture; import android.widget.TextView; import java.util.concurrent.Executor; @@ -275,8 +277,12 @@ public final class EditableInputConnection extends BaseInputConnection int result; if (gesture instanceof SelectGesture) { result = mTextView.performHandwritingSelectGesture((SelectGesture) gesture); + } else if (gesture instanceof SelectRangeGesture) { + result = mTextView.performHandwritingSelectRangeGesture((SelectRangeGesture) gesture); } else if (gesture instanceof DeleteGesture) { result = mTextView.performHandwritingDeleteGesture((DeleteGesture) gesture); + } else if (gesture instanceof DeleteRangeGesture) { + result = mTextView.performHandwritingDeleteRangeGesture((DeleteRangeGesture) gesture); } else if (gesture instanceof InsertGesture) { result = mTextView.performHandwritingInsertGesture((InsertGesture) gesture); } else if (gesture instanceof RemoveSpaceGesture) { diff --git a/core/java/com/android/internal/inputmethod/InputMethodDebug.java b/core/java/com/android/internal/inputmethod/InputMethodDebug.java index 09c97b39e260..1b4afd6dd39f 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodDebug.java +++ b/core/java/com/android/internal/inputmethod/InputMethodDebug.java @@ -49,6 +49,8 @@ public final class InputMethodDebug { return "WINDOW_FOCUS_GAIN"; case StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY: return "WINDOW_FOCUS_GAIN_REPORT_ONLY"; + case StartInputReason.SCHEDULED_CHECK_FOCUS: + return "SCHEDULED_CHECK_FOCUS"; case StartInputReason.APP_CALLED_RESTART_INPUT_API: return "APP_CALLED_RESTART_INPUT_API"; case StartInputReason.CHECK_FOCUS: diff --git a/core/java/com/android/internal/inputmethod/StartInputReason.java b/core/java/com/android/internal/inputmethod/StartInputReason.java index 51ed841410d7..733d9751c810 100644 --- a/core/java/com/android/internal/inputmethod/StartInputReason.java +++ b/core/java/com/android/internal/inputmethod/StartInputReason.java @@ -31,6 +31,7 @@ import java.lang.annotation.Retention; StartInputReason.UNSPECIFIED, StartInputReason.WINDOW_FOCUS_GAIN, StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY, + StartInputReason.SCHEDULED_CHECK_FOCUS, StartInputReason.APP_CALLED_RESTART_INPUT_API, StartInputReason.CHECK_FOCUS, StartInputReason.BOUND_TO_IMMS, @@ -58,6 +59,11 @@ public @interface StartInputReason { */ int WINDOW_FOCUS_GAIN_REPORT_ONLY = 2; /** + * Similar to {@link #CHECK_FOCUS}, but the one scheduled with + * {@link android.view.ViewRootImpl#dispatchCheckFocus()}. + */ + int SCHEDULED_CHECK_FOCUS = 3; + /** * {@link android.view.inputmethod.InputMethodManager#restartInput(android.view.View)} is * either explicitly called by the application or indirectly called by some Framework class * (e.g. {@link android.widget.EditText}). diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index 76f33a6c3937..b0d59226d4ec 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -45,6 +45,7 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__ONE_HANDED_ENTER_TRANSITION; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__ONE_HANDED_EXIT_TRANSITION; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PIP_TRANSITION; +import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF_SHOW_AOD; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_PAGE_SCROLL; @@ -224,6 +225,7 @@ public class InteractionJankMonitor { public static final int CUJ_SHADE_CLEAR_ALL = 62; public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION = 63; public static final int CUJ_LOCKSCREEN_OCCLUSION = 64; + public static final int CUJ_RECENTS_SCROLLING = 65; private static final int NO_STATSD_LOGGING = -1; @@ -297,6 +299,7 @@ public class InteractionJankMonitor { UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_CLEAR_ALL, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_OCCLUSION, + UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING, }; private static class InstanceHolder { @@ -385,7 +388,8 @@ public class InteractionJankMonitor { CUJ_TASKBAR_COLLAPSE, CUJ_SHADE_CLEAR_ALL, CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION, - CUJ_LOCKSCREEN_OCCLUSION + CUJ_LOCKSCREEN_OCCLUSION, + CUJ_RECENTS_SCROLLING }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { @@ -900,6 +904,8 @@ public class InteractionJankMonitor { return "LAUNCHER_UNLOCK_ENTRANCE_ANIMATION"; case CUJ_LOCKSCREEN_OCCLUSION: return "LOCKSCREEN_OCCLUSION"; + case CUJ_RECENTS_SCROLLING: + return "RECENTS_SCROLLING"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java index 681b46a01c8d..0489dc812ab6 100644 --- a/core/java/com/android/internal/notification/SystemNotificationChannels.java +++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java @@ -35,7 +35,10 @@ import java.util.List; // Manages the NotificationChannels used by the frameworks itself. public class SystemNotificationChannels { - public static String VIRTUAL_KEYBOARD = "VIRTUAL_KEYBOARD"; + /** + * @deprecated Legacy system channel, which is no longer used, + */ + @Deprecated public static String VIRTUAL_KEYBOARD = "VIRTUAL_KEYBOARD"; public static String PHYSICAL_KEYBOARD = "PHYSICAL_KEYBOARD"; public static String SECURITY = "SECURITY"; public static String CAR_MODE = "CAR_MODE"; @@ -72,13 +75,6 @@ public class SystemNotificationChannels { public static void createAll(Context context) { final NotificationManager nm = context.getSystemService(NotificationManager.class); List<NotificationChannel> channelsList = new ArrayList<NotificationChannel>(); - final NotificationChannel keyboard = new NotificationChannel( - VIRTUAL_KEYBOARD, - context.getString(R.string.notification_channel_virtual_keyboard), - NotificationManager.IMPORTANCE_LOW); - keyboard.setBlockable(true); - channelsList.add(keyboard); - final NotificationChannel physicalKeyboardChannel = new NotificationChannel( PHYSICAL_KEYBOARD, context.getString(R.string.notification_channel_physical_keyboard), @@ -237,6 +233,7 @@ public class SystemNotificationChannels { /** Remove notification channels which are no longer used */ public static void removeDeprecated(Context context) { final NotificationManager nm = context.getSystemService(NotificationManager.class); + nm.deleteNotificationChannel(VIRTUAL_KEYBOARD); nm.deleteNotificationChannel(DEVICE_ADMIN_DEPRECATED); nm.deleteNotificationChannel(SYSTEM_CHANGES_DEPRECATED); } diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp index 1520ea5c6831..03815108f6dd 100644 --- a/core/jni/android_graphics_BLASTBufferQueue.cpp +++ b/core/jni/android_graphics_BLASTBufferQueue.cpp @@ -71,10 +71,12 @@ public: } } - void onTransactionHang(bool isGpuHang) { + void onTransactionHang(const std::string& reason) { if (mTransactionHangObject) { + JNIEnv* env = getenv(mVm); + ScopedLocalRef<jstring> jReason(env, env->NewStringUTF(reason.c_str())); getenv(mVm)->CallVoidMethod(mTransactionHangObject, - gTransactionHangCallback.onTransactionHang, isGpuHang); + gTransactionHangCallback.onTransactionHang, jReason.get()); } } @@ -177,7 +179,7 @@ static bool nativeIsSameSurfaceControl(JNIEnv* env, jclass clazz, jlong ptr, jlo sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr); return queue->isSameSurfaceControl(reinterpret_cast<SurfaceControl*>(surfaceControl)); } - + static void nativeSetTransactionHangCallback(JNIEnv* env, jclass clazz, jlong ptr, jobject transactionHangCallback) { sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr); @@ -186,9 +188,8 @@ static void nativeSetTransactionHangCallback(JNIEnv* env, jclass clazz, jlong pt } else { sp<TransactionHangCallbackWrapper> wrapper = new TransactionHangCallbackWrapper{env, transactionHangCallback}; - queue->setTransactionHangCallback([wrapper](bool isGpuHang) { - wrapper->onTransactionHang(isGpuHang); - }); + queue->setTransactionHangCallback( + [wrapper](const std::string& reason) { wrapper->onTransactionHang(reason); }); } } @@ -236,7 +237,8 @@ int register_android_graphics_BLASTBufferQueue(JNIEnv* env) { jclass transactionHangClass = FindClassOrDie(env, "android/graphics/BLASTBufferQueue$TransactionHangCallback"); gTransactionHangCallback.onTransactionHang = - GetMethodIDOrDie(env, transactionHangClass, "onTransactionHang", "(Z)V"); + GetMethodIDOrDie(env, transactionHangClass, "onTransactionHang", + "(Ljava/lang/String;)V"); return 0; } diff --git a/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp b/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp index 09f3a727d16e..2437a511238c 100644 --- a/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp +++ b/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp @@ -89,42 +89,39 @@ static sp<Surface> getSurface(JNIEnv* env, jobject surface) { extern "C" { -static jint SurfaceUtils_nativeDetectSurfaceDataspace(JNIEnv* env, jobject thiz, jobject surface) { - ALOGV("nativeDetectSurfaceDataspace"); +static jint SurfaceUtils_nativeDetectSurfaceType(JNIEnv* env, jobject thiz, jobject surface) { + ALOGV("nativeDetectSurfaceType"); sp<ANativeWindow> anw; if ((anw = getNativeWindow(env, surface)) == NULL) { ALOGE("%s: Could not retrieve native window from surface.", __FUNCTION__); return BAD_VALUE; } int32_t fmt = 0; - status_t err = anw->query(anw.get(), NATIVE_WINDOW_DEFAULT_DATASPACE, &fmt); + status_t err = anw->query(anw.get(), NATIVE_WINDOW_FORMAT, &fmt); if (err != NO_ERROR) { - ALOGE("%s: Error while querying surface dataspace %s (%d).", __FUNCTION__, strerror(-err), - err); + ALOGE("%s: Error while querying surface pixel format %s (%d).", __FUNCTION__, + strerror(-err), err); OVERRIDE_SURFACE_ERROR(err); return err; } return fmt; } -static jint SurfaceUtils_nativeDetectSurfaceType(JNIEnv* env, jobject thiz, jobject surface) { - ALOGV("nativeDetectSurfaceType"); +static jint SurfaceUtils_nativeDetectSurfaceDataspace(JNIEnv* env, jobject thiz, jobject surface) { + ALOGV("nativeDetectSurfaceDataspace"); sp<ANativeWindow> anw; if ((anw = getNativeWindow(env, surface)) == NULL) { ALOGE("%s: Could not retrieve native window from surface.", __FUNCTION__); return BAD_VALUE; } - int32_t halFmt = 0; - status_t err = anw->query(anw.get(), NATIVE_WINDOW_FORMAT, &halFmt); + int32_t fmt = 0; + status_t err = anw->query(anw.get(), NATIVE_WINDOW_DEFAULT_DATASPACE, &fmt); if (err != NO_ERROR) { - ALOGE("%s: Error while querying surface pixel format %s (%d).", __FUNCTION__, - strerror(-err), err); + ALOGE("%s: Error while querying surface dataspace %s (%d).", __FUNCTION__, strerror(-err), + err); OVERRIDE_SURFACE_ERROR(err); return err; } - int32_t dataspace = SurfaceUtils_nativeDetectSurfaceDataspace(env, thiz, surface); - int32_t fmt = static_cast<int32_t>( - mapHalFormatDataspaceToPublicFormat(halFmt, static_cast<android_dataspace>(dataspace))); return fmt; } diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp index 40f6e4f63cd7..5c71f692b80f 100644 --- a/core/jni/fd_utils.cpp +++ b/core/jni/fd_utils.cpp @@ -580,6 +580,7 @@ void FileDescriptorTable::RestatInternal(std::set<int>& open_fds, fail_fn_t fail // TODO(narayan): This will be an error in a future android release. // error = true; // ALOGW("Zygote closed file descriptor %d.", it->first); + delete it->second; it = open_fd_map_.erase(it); } else { // The entry from the file descriptor table is still open. Restat diff --git a/core/proto/android/view/imefocuscontroller.proto b/core/proto/android/view/imefocuscontroller.proto index ff9dee69207b..ccde9b7a7966 100644 --- a/core/proto/android/view/imefocuscontroller.proto +++ b/core/proto/android/view/imefocuscontroller.proto @@ -25,6 +25,6 @@ option java_multiple_files = true; */ message ImeFocusControllerProto { optional bool has_ime_focus = 1; - optional string served_view = 2; - optional string next_served_view = 3; + optional string served_view = 2 [deprecated = true]; + optional string next_served_view = 3 [deprecated = true]; }
\ No newline at end of file diff --git a/core/proto/android/view/inputmethod/inputmethodmanager.proto b/core/proto/android/view/inputmethod/inputmethodmanager.proto index 9fed0ef95a27..ea5f1e8f3be2 100644 --- a/core/proto/android/view/inputmethod/inputmethodmanager.proto +++ b/core/proto/android/view/inputmethod/inputmethodmanager.proto @@ -29,4 +29,6 @@ message InputMethodManagerProto { optional int32 display_id = 3; optional bool active = 4; optional bool served_connecting = 5; + optional string served_view = 6; + optional string next_served_view = 7; }
\ No newline at end of file diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 1f23eb6a86d6..eb54abfbbcbe 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -668,7 +668,6 @@ <protected-broadcast android:name="android.media.tv.action.PREVIEW_PROGRAM_BROWSABLE_DISABLED" /> <protected-broadcast android:name="android.media.tv.action.WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED" /> <protected-broadcast android:name="android.media.tv.action.CHANNEL_BROWSABLE_REQUESTED" /> - <protected-broadcast android:name="com.android.server.inputmethod.InputMethodManagerService.SHOW_INPUT_METHOD_PICKER" /> <!-- Time zone rules update intents fired by the system server --> <protected-broadcast android:name="com.android.intent.action.timezone.RULES_UPDATE_OPERATION" /> @@ -4261,6 +4260,13 @@ <permission android:name="android.permission.BIND_AUTOFILL_SERVICE" android:protectionLevel="signature" /> + <!-- Must be required by a CredentialProviderService to ensure that only the + system can bind to it. + <p>Protection level: signature + --> + <permission android:name="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE" + android:protectionLevel="signature" /> + <!-- Alternative version of android.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE. This permission was renamed during the O previews but it was supported on the final O release, so we need to carry it over. @@ -6571,6 +6577,13 @@ android:protectionLevel="signature" /> <uses-permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART" /> + <!-- Allows financed device kiosk apps to perform actions on the Device Lock service + <p>Protection level: internal|role + <p>Intended for use by the FINANCED_DEVICE_KIOSK role only. + --> + <permission android:name="android.permission.MANAGE_DEVICE_LOCK_STATE" + android:protectionLevel="internal|role" /> + <!-- Attribution for Geofencing service. --> <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/> <!-- Attribution for Country Detector. --> diff --git a/core/res/res/drawable-hdpi/ic_notification_ime_default.png b/core/res/res/drawable-hdpi/ic_notification_ime_default.png Binary files differdeleted file mode 100644 index 369c88d31707..000000000000 --- a/core/res/res/drawable-hdpi/ic_notification_ime_default.png +++ /dev/null diff --git a/core/res/res/drawable-mdpi/ic_notification_ime_default.png b/core/res/res/drawable-mdpi/ic_notification_ime_default.png Binary files differdeleted file mode 100644 index 7d97eb575f2a..000000000000 --- a/core/res/res/drawable-mdpi/ic_notification_ime_default.png +++ /dev/null diff --git a/core/res/res/drawable-xhdpi/ic_notification_ime_default.png b/core/res/res/drawable-xhdpi/ic_notification_ime_default.png Binary files differdeleted file mode 100644 index 900801a4d3d7..000000000000 --- a/core/res/res/drawable-xhdpi/ic_notification_ime_default.png +++ /dev/null diff --git a/core/res/res/drawable-xxhdpi/ic_notification_ime_default.png b/core/res/res/drawable-xxhdpi/ic_notification_ime_default.png Binary files differdeleted file mode 100644 index 6c8222ec50b7..000000000000 --- a/core/res/res/drawable-xxhdpi/ic_notification_ime_default.png +++ /dev/null diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml index a7f2aa7cba69..be1c939f0ff8 100644 --- a/core/res/res/layout/notification_template_header.xml +++ b/core/res/res/layout/notification_template_header.xml @@ -24,6 +24,7 @@ android:gravity="center_vertical" android:orientation="horizontal" android:theme="@style/Theme.DeviceDefault.Notification" + android:importantForAccessibility="no" > <ImageView diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index b5cdcff76ed1..caa67de1fe61 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3559,9 +3559,9 @@ config_sidefpsSkipWaitForPowerVendorAcquireMessage --> <integer name="config_sidefpsSkipWaitForPowerAcquireMessage">6</integer> - <!-- This vendor acquired message that will cause the sidefpsKgPowerPress window to be skipped. - config_sidefpsSkipWaitForPowerOnFingerUp must be true and - config_sidefpsSkipWaitForPowerAcquireMessage must be BIOMETRIC_ACQUIRED_VENDOR == 6. --> + <!-- This vendor acquired message will cause the sidefpsKgPowerPress window to be skipped + when config_sidefpsSkipWaitForPowerAcquireMessage == 6 (VENDOR) and the vendor acquire + message equals this constant --> <integer name="config_sidefpsSkipWaitForPowerVendorAcquireMessage">2</integer> <!-- This config is used to force VoiceInteractionService to start on certain low ram devices. diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index a1d73ff25cb8..71b2f00ff2b1 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -117,4 +117,17 @@ <!-- Whether using the new SubscriptionManagerService or the old SubscriptionController --> <bool name="config_using_subscription_manager_service">false</bool> <java-symbol type="bool" name="config_using_subscription_manager_service" /> + + <!-- Boolean indicating whether the emergency numbers for a country, sourced from modem/config, + should be ignored if that country is 'locked' (i.e. ignore_modem_config set to true) in + Android Emergency DB. If this value is true, emergency numbers for a country, sourced from + modem/config, will be ignored if that country is 'locked' in Android Emergency DB. --> + <bool name="ignore_modem_config_emergency_numbers">false</bool> + <java-symbol type="bool" name="ignore_modem_config_emergency_numbers" /> + + <!-- Boolean indicating whether emergency numbers routing from the android emergency number + database should be ignored (i.e. routing will always be set to UNKNOWN). If this value is + true, routing from the android emergency number database will be ignored. --> + <bool name="ignore_emergency_number_routing_from_db">false</bool> + <java-symbol type="bool" name="ignore_emergency_number_routing_from_db" /> </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 5f9911358921..d0fca8b0a7bb 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -741,9 +741,6 @@ <!-- Text shown in place of notification contents when the notification is hidden on a secure lockscreen --> <string name="notification_hidden_text">New notification</string> - <!-- Text shown when viewing channel settings for notifications related to the virtual keyboard --> - <string name="notification_channel_virtual_keyboard">Virtual keyboard</string> - <!-- Text shown when viewing channel settings for notifications related to the hardware keyboard --> <string name="notification_channel_physical_keyboard">Physical keyboard</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index b94d7990b394..fc55ed2fe443 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1992,7 +1992,6 @@ <java-symbol type="color" name="config_defaultNotificationColor" /> <java-symbol type="color" name="decor_view_status_guard" /> <java-symbol type="color" name="decor_view_status_guard_light" /> - <java-symbol type="drawable" name="ic_notification_ime_default" /> <java-symbol type="drawable" name="ic_menu_refresh" /> <java-symbol type="drawable" name="ic_settings" /> <java-symbol type="drawable" name="ic_voice_search" /> diff --git a/core/tests/BroadcastRadioTests/Android.bp b/core/tests/BroadcastRadioTests/Android.bp index 113f45dfef55..7cb64c8b4f0c 100644 --- a/core/tests/BroadcastRadioTests/Android.bp +++ b/core/tests/BroadcastRadioTests/Android.bp @@ -23,23 +23,32 @@ package { android_test { name: "BroadcastRadioTests", + srcs: ["src/**/*.java"], privileged: true, certificate: "platform", // TODO(b/13282254): uncomment when b/13282254 is fixed // sdk_version: "current" platform_apis: true, - static_libs: [ - "compatibility-device-util-axt", - "androidx.test.rules", - "testng", - "services.core", - ], - libs: ["android.test.base"], - srcs: ["src/**/*.java"], dex_preopt: { enabled: false, }, optimize: { enabled: false, }, + static_libs: [ + "services.core", + "androidx.test.rules", + "truth-prebuilt", + "testng", + "mockito-target-extended", + ], + libs: ["android.test.base"], + test_suites: [ + "general-tests", + ], + // mockito-target-inline dependency + jni_libs: [ + "libcarservicejni", + "libdexmakerjvmtiagent", + ], } diff --git a/core/tests/BroadcastRadioTests/AndroidManifest.xml b/core/tests/BroadcastRadioTests/AndroidManifest.xml index ce12cc99946f..869b4844e529 100644 --- a/core/tests/BroadcastRadioTests/AndroidManifest.xml +++ b/core/tests/BroadcastRadioTests/AndroidManifest.xml @@ -19,7 +19,7 @@ <uses-permission android:name="android.permission.ACCESS_BROADCAST_RADIO" /> - <application> + <application android:debuggable="true"> <uses-library android:name="android.test.runner" /> </application> diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java index 11eb158317e9..3f35e998e025 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java @@ -33,11 +33,9 @@ import android.content.pm.PackageManager; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.hardware.radio.RadioTuner; -import android.test.suitebuilder.annotation.MediumTest; import android.util.Log; import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; @@ -47,6 +45,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; import java.util.ArrayList; import java.util.HashMap; @@ -56,8 +55,7 @@ import java.util.Map; /** * A test for broadcast radio API. */ -@RunWith(AndroidJUnit4.class) -@MediumTest +@RunWith(MockitoJUnitRunner.class) public class RadioTunerTest { private static final String TAG = "BroadcastRadioTests.RadioTuner"; diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java new file mode 100644 index 000000000000..42143b92e9d8 --- /dev/null +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.radio.tests.unittests; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertThrows; + +import android.hardware.radio.Announcement; +import android.hardware.radio.ProgramSelector; +import android.util.ArrayMap; + +import org.junit.Test; + +import java.util.Map; + +public final class RadioAnnouncementTest { + private static final ProgramSelector.Identifier FM_IDENTIFIER = new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 90500); + private static final ProgramSelector FM_PROGRAM_SELECTOR = new ProgramSelector( + ProgramSelector.PROGRAM_TYPE_FM, FM_IDENTIFIER, /* secondaryIds= */ null, + /* vendorIds= */ null); + private static final int TRAFFIC_ANNOUNCEMENT_TYPE = Announcement.TYPE_TRAFFIC; + private static final Map<String, String> VENDOR_INFO = createVendorInfo(); + private static final Announcement TEST_ANNOUNCEMENT = + new Announcement(FM_PROGRAM_SELECTOR, TRAFFIC_ANNOUNCEMENT_TYPE, VENDOR_INFO); + + @Test + public void constructor_withNullSelector_fails() { + NullPointerException thrown = assertThrows(NullPointerException.class, () -> { + new Announcement(/* selector= */ null, TRAFFIC_ANNOUNCEMENT_TYPE, VENDOR_INFO); + }); + + assertWithMessage("Exception for null program selector in announcement constructor") + .that(thrown).hasMessageThat().contains("Program selector cannot be null"); + } + + @Test + public void constructor_withNullVendorInfo_fails() { + NullPointerException thrown = assertThrows(NullPointerException.class, () -> { + new Announcement(FM_PROGRAM_SELECTOR, TRAFFIC_ANNOUNCEMENT_TYPE, + /* vendorInfo= */ null); + }); + + assertWithMessage("Exception for null vendor info in announcement constructor") + .that(thrown).hasMessageThat().contains("Vendor info cannot be null"); + } + + @Test + public void getSelector() { + assertWithMessage("Radio announcement selector") + .that(TEST_ANNOUNCEMENT.getSelector()).isEqualTo(FM_PROGRAM_SELECTOR); + } + + @Test + public void getType() { + assertWithMessage("Radio announcement type") + .that(TEST_ANNOUNCEMENT.getType()).isEqualTo(TRAFFIC_ANNOUNCEMENT_TYPE); + } + + @Test + public void getVendorInfo() { + assertWithMessage("Radio announcement vendor info") + .that(TEST_ANNOUNCEMENT.getVendorInfo()).isEqualTo(VENDOR_INFO); + } + + private static Map<String, String> createVendorInfo() { + Map<String, String> vendorInfo = new ArrayMap<>(); + vendorInfo.put("vendorKeyMock", "vendorValueMock"); + return vendorInfo; + } +} diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java new file mode 100644 index 000000000000..9bfa2fba6948 --- /dev/null +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java @@ -0,0 +1,648 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.radio.tests.unittests; + +import static com.google.common.truth.Truth.assertWithMessage; + +import android.hardware.radio.ProgramSelector; +import android.hardware.radio.RadioManager; +import android.hardware.radio.RadioMetadata; + +import org.junit.Test; + +import java.util.Arrays; + +public final class RadioManagerTest { + + private static final int REGION = RadioManager.REGION_ITU_2; + private static final int FM_LOWER_LIMIT = 87500; + private static final int FM_UPPER_LIMIT = 108000; + private static final int FM_SPACING = 200; + private static final int AM_LOWER_LIMIT = 540; + private static final int AM_UPPER_LIMIT = 1700; + private static final int AM_SPACING = 10; + private static final boolean STEREO_SUPPORTED = true; + private static final boolean RDS_SUPPORTED = true; + private static final boolean TA_SUPPORTED = false; + private static final boolean AF_SUPPORTED = false; + private static final boolean EA_SUPPORTED = false; + + private static final int PROPERTIES_ID = 10; + private static final String SERVICE_NAME = "ServiceNameMock"; + private static final int CLASS_ID = RadioManager.CLASS_AM_FM; + private static final String IMPLEMENTOR = "ImplementorMock"; + private static final String PRODUCT = "ProductMock"; + private static final String VERSION = "VersionMock"; + private static final String SERIAL = "SerialMock"; + private static final int NUM_TUNERS = 1; + private static final int NUM_AUDIO_SOURCES = 1; + private static final boolean IS_INITIALIZATION_REQUIRED = false; + private static final boolean IS_CAPTURE_SUPPORTED = false; + private static final boolean IS_BG_SCAN_SUPPORTED = true; + private static final int[] SUPPORTED_PROGRAM_TYPES = new int[]{ + ProgramSelector.PROGRAM_TYPE_AM, ProgramSelector.PROGRAM_TYPE_FM}; + private static final int[] SUPPORTED_IDENTIFIERS_TYPES = new int[]{ + ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, ProgramSelector.IDENTIFIER_TYPE_RDS_PI}; + + private static final RadioManager.FmBandDescriptor FM_BAND_DESCRIPTOR = + createFmBandDescriptor(); + private static final RadioManager.AmBandDescriptor AM_BAND_DESCRIPTOR = + createAmBandDescriptor(); + private static final RadioManager.FmBandConfig FM_BAND_CONFIG = createFmBandConfig(); + private static final RadioManager.AmBandConfig AM_BAND_CONFIG = createAmBandConfig(); + private static final RadioManager.ModuleProperties AMFM_PROPERTIES = createAmFmProperties(); + + /** + * Info flags with live, tuned and stereo enabled + */ + private static final int INFO_FLAGS = 0b110001; + private static final int SIGNAL_QUALITY = 2; + private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, + /* value= */ 0x10000111); + private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_RELATED = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, + /* value= */ 0x10000113); + private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, + /* value= */ 0x1013); + private static final ProgramSelector.Identifier DAB_FREQUENCY_IDENTIFIER = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, + /* value= */ 95500); + private static final ProgramSelector DAB_SELECTOR = + new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB, DAB_SID_EXT_IDENTIFIER, + new ProgramSelector.Identifier[]{ + DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER}, + /* vendorIds= */ null); + private static final RadioMetadata METADATA = createMetadata(); + private static final RadioManager.ProgramInfo DAB_PROGRAM_INFO = + createDabProgramInfo(DAB_SELECTOR); + + @Test + public void getType_forBandDescriptor() { + RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor(); + + assertWithMessage("AM Band Descriptor type") + .that(bandDescriptor.getType()).isEqualTo(RadioManager.BAND_AM); + } + + @Test + public void getRegion_forBandDescriptor() { + RadioManager.BandDescriptor bandDescriptor = createFmBandDescriptor(); + + assertWithMessage("FM Band Descriptor region") + .that(bandDescriptor.getRegion()).isEqualTo(REGION); + } + + @Test + public void getLowerLimit_forBandDescriptor() { + RadioManager.BandDescriptor bandDescriptor = createFmBandDescriptor(); + + assertWithMessage("FM Band Descriptor lower limit") + .that(bandDescriptor.getLowerLimit()).isEqualTo(FM_LOWER_LIMIT); + } + + @Test + public void getUpperLimit_forBandDescriptor() { + RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor(); + + assertWithMessage("AM Band Descriptor upper limit") + .that(bandDescriptor.getUpperLimit()).isEqualTo(AM_UPPER_LIMIT); + } + + @Test + public void getSpacing_forBandDescriptor() { + RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor(); + + assertWithMessage("AM Band Descriptor spacing") + .that(bandDescriptor.getSpacing()).isEqualTo(AM_SPACING); + } + + @Test + public void isAmBand_forAmBandDescriptor_returnsTrue() { + RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor(); + + assertWithMessage("Is AM Band Descriptor an AM band") + .that(bandDescriptor.isAmBand()).isTrue(); + } + + @Test + public void isFmBand_forAmBandDescriptor_returnsFalse() { + RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor(); + + assertWithMessage("Is AM Band Descriptor an FM band") + .that(bandDescriptor.isFmBand()).isFalse(); + } + + @Test + public void isStereoSupported_forFmBandDescriptor() { + assertWithMessage("FM Band Descriptor stereo") + .that(FM_BAND_DESCRIPTOR.isStereoSupported()).isEqualTo(STEREO_SUPPORTED); + } + + @Test + public void isRdsSupported_forFmBandDescriptor() { + assertWithMessage("FM Band Descriptor RDS or RBDS") + .that(FM_BAND_DESCRIPTOR.isRdsSupported()).isEqualTo(RDS_SUPPORTED); + } + + @Test + public void isTaSupported_forFmBandDescriptor() { + assertWithMessage("FM Band Descriptor traffic announcement") + .that(FM_BAND_DESCRIPTOR.isTaSupported()).isEqualTo(TA_SUPPORTED); + } + + @Test + public void isAfSupported_forFmBandDescriptor() { + assertWithMessage("FM Band Descriptor alternate frequency") + .that(FM_BAND_DESCRIPTOR.isAfSupported()).isEqualTo(AF_SUPPORTED); + } + + @Test + public void isEaSupported_forFmBandDescriptor() { + assertWithMessage("FM Band Descriptor emergency announcement") + .that(FM_BAND_DESCRIPTOR.isEaSupported()).isEqualTo(EA_SUPPORTED); + } + + @Test + public void isStereoSupported_forAmBandDescriptor() { + assertWithMessage("AM Band Descriptor stereo") + .that(AM_BAND_DESCRIPTOR.isStereoSupported()).isEqualTo(STEREO_SUPPORTED); + } + + @Test + public void equals_withSameFmBandDescriptors_returnsTrue() { + RadioManager.FmBandDescriptor fmBandDescriptor1 = createFmBandDescriptor(); + RadioManager.FmBandDescriptor fmBandDescriptor2 = createFmBandDescriptor(); + + assertWithMessage("The same FM Band Descriptor") + .that(fmBandDescriptor1).isEqualTo(fmBandDescriptor2); + } + + @Test + public void equals_withSameAmBandDescriptors_returnsTrue() { + RadioManager.AmBandDescriptor amBandDescriptorCompared = createAmBandDescriptor(); + + assertWithMessage("The same AM Band Descriptor") + .that(AM_BAND_DESCRIPTOR).isEqualTo(amBandDescriptorCompared); + } + + @Test + public void equals_withAmBandDescriptorsOfDifferentUpperLimits_returnsFalse() { + RadioManager.AmBandDescriptor amBandDescriptorCompared = + new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT, + AM_UPPER_LIMIT + AM_SPACING, AM_SPACING, STEREO_SUPPORTED); + + assertWithMessage("AM Band Descriptor of different upper limit") + .that(AM_BAND_DESCRIPTOR).isNotEqualTo(amBandDescriptorCompared); + } + + @Test + public void equals_withAmAndFmBandDescriptors_returnsFalse() { + assertWithMessage("AM Band Descriptor") + .that(AM_BAND_DESCRIPTOR).isNotEqualTo(FM_BAND_DESCRIPTOR); + } + + @Test + public void getType_forBandConfig() { + RadioManager.BandConfig fmBandConfig = createFmBandConfig(); + + assertWithMessage("FM Band Config type") + .that(fmBandConfig.getType()).isEqualTo(RadioManager.BAND_FM); + } + + @Test + public void getRegion_forBandConfig() { + RadioManager.BandConfig amBandConfig = createAmBandConfig(); + + assertWithMessage("AM Band Config region") + .that(amBandConfig.getRegion()).isEqualTo(REGION); + } + + @Test + public void getLowerLimit_forBandConfig() { + RadioManager.BandConfig amBandConfig = createAmBandConfig(); + + assertWithMessage("AM Band Config lower limit") + .that(amBandConfig.getLowerLimit()).isEqualTo(AM_LOWER_LIMIT); + } + + @Test + public void getUpperLimit_forBandConfig() { + RadioManager.BandConfig fmBandConfig = createFmBandConfig(); + + assertWithMessage("FM Band Config upper limit") + .that(fmBandConfig.getUpperLimit()).isEqualTo(FM_UPPER_LIMIT); + } + + @Test + public void getSpacing_forBandConfig() { + RadioManager.BandConfig fmBandConfig = createFmBandConfig(); + + assertWithMessage("FM Band Config spacing") + .that(fmBandConfig.getSpacing()).isEqualTo(FM_SPACING); + } + + @Test + public void getStereo_forFmBandConfig() { + assertWithMessage("FM Band Config stereo ") + .that(FM_BAND_CONFIG.getStereo()).isEqualTo(STEREO_SUPPORTED); + } + + @Test + public void getRds_forFmBandConfig() { + assertWithMessage("FM Band Config RDS or RBDS") + .that(FM_BAND_CONFIG.getRds()).isEqualTo(RDS_SUPPORTED); + } + + @Test + public void getTa_forFmBandConfig() { + assertWithMessage("FM Band Config traffic announcement") + .that(FM_BAND_CONFIG.getTa()).isEqualTo(TA_SUPPORTED); + } + + @Test + public void getAf_forFmBandConfig() { + assertWithMessage("FM Band Config alternate frequency") + .that(FM_BAND_CONFIG.getAf()).isEqualTo(AF_SUPPORTED); + } + + @Test + public void getEa_forFmBandConfig() { + assertWithMessage("FM Band Config emergency Announcement") + .that(FM_BAND_CONFIG.getEa()).isEqualTo(EA_SUPPORTED); + } + + @Test + public void getStereo_forAmBandConfig() { + assertWithMessage("AM Band Config stereo") + .that(AM_BAND_CONFIG.getStereo()).isEqualTo(STEREO_SUPPORTED); + } + + @Test + public void equals_withSameFmBandConfigs_returnsTrue() { + RadioManager.FmBandConfig fmBandConfigCompared = createFmBandConfig(); + + assertWithMessage("The same FM Band Config") + .that(FM_BAND_CONFIG).isEqualTo(fmBandConfigCompared); + } + + @Test + public void equals_withFmBandConfigsOfDifferentAfs_returnsFalse() { + RadioManager.FmBandConfig.Builder builder = new RadioManager.FmBandConfig.Builder( + createFmBandDescriptor()).setStereo(STEREO_SUPPORTED).setRds(RDS_SUPPORTED) + .setTa(TA_SUPPORTED).setAf(!AF_SUPPORTED).setEa(EA_SUPPORTED); + RadioManager.FmBandConfig fmBandConfigFromBuilder = builder.build(); + + assertWithMessage("FM Band Config of different af value") + .that(FM_BAND_CONFIG).isNotEqualTo(fmBandConfigFromBuilder); + } + + @Test + public void equals_withFmAndAmBandConfigs_returnsFalse() { + assertWithMessage("FM Band Config") + .that(FM_BAND_CONFIG).isNotEqualTo(AM_BAND_CONFIG); + } + + @Test + public void equals_withSameAmBandConfigs_returnsTrue() { + RadioManager.AmBandConfig amBandConfigCompared = createAmBandConfig(); + + assertWithMessage("The same AM Band Config") + .that(AM_BAND_CONFIG).isEqualTo(amBandConfigCompared); + } + + @Test + public void equals_withAmBandConfigsOfDifferentTypes_returnsFalse() { + RadioManager.AmBandConfig amBandConfigCompared = new RadioManager.AmBandConfig( + new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM_HD, AM_LOWER_LIMIT, + AM_UPPER_LIMIT, AM_SPACING, STEREO_SUPPORTED)); + + assertWithMessage("AM Band Config of different type") + .that(AM_BAND_CONFIG).isNotEqualTo(amBandConfigCompared); + } + + @Test + public void equals_withAmBandConfigsOfDifferentStereoValues_returnsFalse() { + RadioManager.AmBandConfig.Builder builder = new RadioManager.AmBandConfig.Builder( + createAmBandDescriptor()).setStereo(!STEREO_SUPPORTED); + RadioManager.AmBandConfig amBandConfigFromBuilder = builder.build(); + + assertWithMessage("AM Band Config of different stereo value") + .that(AM_BAND_CONFIG).isNotEqualTo(amBandConfigFromBuilder); + } + + @Test + public void getId_forModuleProperties() { + assertWithMessage("Properties id") + .that(AMFM_PROPERTIES.getId()).isEqualTo(PROPERTIES_ID); + } + + @Test + public void getServiceName_forModuleProperties() { + assertWithMessage("Properties service name") + .that(AMFM_PROPERTIES.getServiceName()).isEqualTo(SERVICE_NAME); + } + + @Test + public void getClassId_forModuleProperties() { + assertWithMessage("Properties class ID") + .that(AMFM_PROPERTIES.getClassId()).isEqualTo(CLASS_ID); + } + + @Test + public void getImplementor_forModuleProperties() { + assertWithMessage("Properties implementor") + .that(AMFM_PROPERTIES.getImplementor()).isEqualTo(IMPLEMENTOR); + } + + @Test + public void getProduct_forModuleProperties() { + assertWithMessage("Properties product") + .that(AMFM_PROPERTIES.getProduct()).isEqualTo(PRODUCT); + } + + @Test + public void getVersion_forModuleProperties() { + assertWithMessage("Properties version") + .that(AMFM_PROPERTIES.getVersion()).isEqualTo(VERSION); + } + + @Test + public void getSerial_forModuleProperties() { + assertWithMessage("Serial properties") + .that(AMFM_PROPERTIES.getSerial()).isEqualTo(SERIAL); + } + + @Test + public void getNumTuners_forModuleProperties() { + assertWithMessage("Number of tuners in properties") + .that(AMFM_PROPERTIES.getNumTuners()).isEqualTo(NUM_TUNERS); + } + + @Test + public void getNumAudioSources_forModuleProperties() { + assertWithMessage("Number of audio sources in properties") + .that(AMFM_PROPERTIES.getNumAudioSources()).isEqualTo(NUM_AUDIO_SOURCES); + } + + @Test + public void isInitializationRequired_forModuleProperties() { + assertWithMessage("Initialization required in properties") + .that(AMFM_PROPERTIES.isInitializationRequired()) + .isEqualTo(IS_INITIALIZATION_REQUIRED); + } + + @Test + public void isCaptureSupported_forModuleProperties() { + assertWithMessage("Capture support in properties") + .that(AMFM_PROPERTIES.isCaptureSupported()).isEqualTo(IS_CAPTURE_SUPPORTED); + } + + @Test + public void isBackgroundScanningSupported_forModuleProperties() { + assertWithMessage("Background scan support in properties") + .that(AMFM_PROPERTIES.isBackgroundScanningSupported()) + .isEqualTo(IS_BG_SCAN_SUPPORTED); + } + + @Test + public void isProgramTypeSupported_withSupportedType_forModuleProperties() { + assertWithMessage("AM/FM frequency type radio support in properties") + .that(AMFM_PROPERTIES.isProgramTypeSupported( + ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)) + .isTrue(); + } + + @Test + public void isProgramTypeSupported_withNonSupportedType_forModuleProperties() { + assertWithMessage("DAB frequency type radio support in properties") + .that(AMFM_PROPERTIES.isProgramTypeSupported( + ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY)).isFalse(); + } + + @Test + public void isProgramIdentifierSupported_withSupportedIdentifier_forModuleProperties() { + assertWithMessage("AM/FM frequency identifier radio support in properties") + .that(AMFM_PROPERTIES.isProgramIdentifierSupported( + ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)).isTrue(); + } + + @Test + public void isProgramIdentifierSupported_withNonSupportedIdentifier_forModuleProperties() { + assertWithMessage("DAB frequency identifier radio support in properties") + .that(AMFM_PROPERTIES.isProgramIdentifierSupported( + ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY)).isFalse(); + } + + @Test + public void getDabFrequencyTable_forModuleProperties() { + assertWithMessage("Properties DAB frequency table") + .that(AMFM_PROPERTIES.getDabFrequencyTable()).isNull(); + } + + @Test + public void getVendorInfo_forModuleProperties() { + assertWithMessage("Properties vendor info") + .that(AMFM_PROPERTIES.getVendorInfo()).isEmpty(); + } + + @Test + public void getBands_forModuleProperties() { + assertWithMessage("Properties bands") + .that(AMFM_PROPERTIES.getBands()).asList() + .containsExactly(AM_BAND_DESCRIPTOR, FM_BAND_DESCRIPTOR); + } + + @Test + public void equals_withSameProperties_returnsTrue() { + RadioManager.ModuleProperties propertiesCompared = createAmFmProperties(); + + assertWithMessage("The same module properties") + .that(AMFM_PROPERTIES).isEqualTo(propertiesCompared); + } + + @Test + public void equals_withModulePropertiesOfDifferentIds_returnsFalse() { + RadioManager.ModuleProperties propertiesDab = new RadioManager.ModuleProperties( + PROPERTIES_ID + 1, SERVICE_NAME, CLASS_ID, IMPLEMENTOR, PRODUCT, VERSION, + SERIAL, NUM_TUNERS, NUM_AUDIO_SOURCES, IS_INITIALIZATION_REQUIRED, + IS_CAPTURE_SUPPORTED, /* bands= */ null, IS_BG_SCAN_SUPPORTED, + SUPPORTED_PROGRAM_TYPES, SUPPORTED_IDENTIFIERS_TYPES, /* dabFrequencyTable= */ null, + /* vendorInfo= */ null); + + assertWithMessage("Module properties of different id") + .that(AMFM_PROPERTIES).isNotEqualTo(propertiesDab); + } + + @Test + public void getSelector_forProgramInfo() { + assertWithMessage("Selector of DAB program info") + .that(DAB_PROGRAM_INFO.getSelector()).isEqualTo(DAB_SELECTOR); + } + + @Test + public void getLogicallyTunedTo_forProgramInfo() { + assertWithMessage("Identifier logically tuned to in DAB program info") + .that(DAB_PROGRAM_INFO.getLogicallyTunedTo()).isEqualTo(DAB_FREQUENCY_IDENTIFIER); + } + + @Test + public void getPhysicallyTunedTo_forProgramInfo() { + assertWithMessage("Identifier physically tuned to DAB program info") + .that(DAB_PROGRAM_INFO.getPhysicallyTunedTo()).isEqualTo(DAB_SID_EXT_IDENTIFIER); + } + + @Test + public void getRelatedContent_forProgramInfo() { + assertWithMessage("Related contents of DAB program info") + .that(DAB_PROGRAM_INFO.getRelatedContent()) + .containsExactly(DAB_SID_EXT_IDENTIFIER_RELATED); + } + + @Test + public void getChannel_forProgramInfo() { + assertWithMessage("Main channel of DAB program info") + .that(DAB_PROGRAM_INFO.getChannel()).isEqualTo(0); + } + + @Test + public void getSubChannel_forProgramInfo() { + assertWithMessage("Sub channel of DAB program info") + .that(DAB_PROGRAM_INFO.getSubChannel()).isEqualTo(0); + } + + @Test + public void isTuned_forProgramInfo() { + assertWithMessage("Tuned status of DAB program info") + .that(DAB_PROGRAM_INFO.isTuned()).isTrue(); + } + + @Test + public void isStereo_forProgramInfo() { + assertWithMessage("Stereo support in DAB program info") + .that(DAB_PROGRAM_INFO.isStereo()).isTrue(); + } + + @Test + public void isDigital_forProgramInfo() { + assertWithMessage("Digital DAB program info") + .that(DAB_PROGRAM_INFO.isDigital()).isTrue(); + } + + @Test + public void isLive_forProgramInfo() { + assertWithMessage("Live status of DAB program info") + .that(DAB_PROGRAM_INFO.isLive()).isTrue(); + } + + @Test + public void isMuted_forProgramInfo() { + assertWithMessage("Muted status of DAB program info") + .that(DAB_PROGRAM_INFO.isMuted()).isFalse(); + } + + @Test + public void isTrafficProgram_forProgramInfo() { + assertWithMessage("Traffic program support in DAB program info") + .that(DAB_PROGRAM_INFO.isTrafficProgram()).isFalse(); + } + + @Test + public void isTrafficAnnouncementActive_forProgramInfo() { + assertWithMessage("Active traffic announcement for DAB program info") + .that(DAB_PROGRAM_INFO.isTrafficAnnouncementActive()).isFalse(); + } + + @Test + public void getSignalStrength_forProgramInfo() { + assertWithMessage("Signal strength of DAB program info") + .that(DAB_PROGRAM_INFO.getSignalStrength()).isEqualTo(SIGNAL_QUALITY); + } + + @Test + public void getMetadata_forProgramInfo() { + assertWithMessage("Metadata of DAB program info") + .that(DAB_PROGRAM_INFO.getMetadata()).isEqualTo(METADATA); + } + + @Test + public void getVendorInfo_forProgramInfo() { + assertWithMessage("Vendor info of DAB program info") + .that(DAB_PROGRAM_INFO.getVendorInfo()).isEmpty(); + } + + @Test + public void equals_withSameProgramInfo_returnsTrue() { + RadioManager.ProgramInfo dabProgramInfoCompared = createDabProgramInfo(DAB_SELECTOR); + + assertWithMessage("The same program info") + .that(dabProgramInfoCompared).isEqualTo(DAB_PROGRAM_INFO); + } + + @Test + public void equals_withSameProgramInfoOfDifferentSecondaryIdSelectors_returnsFalse() { + ProgramSelector dabSelectorCompared = new ProgramSelector( + ProgramSelector.PROGRAM_TYPE_DAB, DAB_SID_EXT_IDENTIFIER, + new ProgramSelector.Identifier[]{DAB_FREQUENCY_IDENTIFIER}, + /* vendorIds= */ null); + RadioManager.ProgramInfo dabProgramInfoCompared = createDabProgramInfo(dabSelectorCompared); + + assertWithMessage("Program info with different secondary id selectors") + .that(DAB_PROGRAM_INFO).isNotEqualTo(dabProgramInfoCompared); + } + + private static RadioManager.ModuleProperties createAmFmProperties() { + return new RadioManager.ModuleProperties(PROPERTIES_ID, SERVICE_NAME, CLASS_ID, + IMPLEMENTOR, PRODUCT, VERSION, SERIAL, NUM_TUNERS, NUM_AUDIO_SOURCES, + IS_INITIALIZATION_REQUIRED, IS_CAPTURE_SUPPORTED, + new RadioManager.BandDescriptor[]{AM_BAND_DESCRIPTOR, FM_BAND_DESCRIPTOR}, + IS_BG_SCAN_SUPPORTED, SUPPORTED_PROGRAM_TYPES, SUPPORTED_IDENTIFIERS_TYPES, + /* dabFrequencyTable= */ null, /* vendorInfo= */ null); + } + + private static RadioManager.FmBandDescriptor createFmBandDescriptor() { + return new RadioManager.FmBandDescriptor(REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, + FM_UPPER_LIMIT, FM_SPACING, STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED, + AF_SUPPORTED, EA_SUPPORTED); + } + + private static RadioManager.AmBandDescriptor createAmBandDescriptor() { + return new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT, + AM_UPPER_LIMIT, AM_SPACING, STEREO_SUPPORTED); + } + + private static RadioManager.FmBandConfig createFmBandConfig() { + return new RadioManager.FmBandConfig(createFmBandDescriptor()); + } + + private static RadioManager.AmBandConfig createAmBandConfig() { + return new RadioManager.AmBandConfig(createAmBandDescriptor()); + } + + private static RadioMetadata createMetadata() { + RadioMetadata.Builder metadataBuilder = new RadioMetadata.Builder(); + return metadataBuilder.putString(RadioMetadata.METADATA_KEY_ARTIST, "artistTest").build(); + } + + private static RadioManager.ProgramInfo createDabProgramInfo(ProgramSelector selector) { + return new RadioManager.ProgramInfo(selector, DAB_FREQUENCY_IDENTIFIER, + DAB_SID_EXT_IDENTIFIER, Arrays.asList(DAB_SID_EXT_IDENTIFIER_RELATED), INFO_FLAGS, + SIGNAL_QUALITY, METADATA, /* vendorInfo= */ null); + } + +} diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java new file mode 100644 index 000000000000..7f4ea1170084 --- /dev/null +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.broadcastradio; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.radio.Announcement; +import android.hardware.radio.IAnnouncementListener; +import android.hardware.radio.ICloseHandle; +import android.hardware.radio.ITuner; +import android.hardware.radio.ITunerCallback; +import android.hardware.radio.RadioManager; + +import com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; + +/** + * Tests for {@link android.hardware.radio.IRadioService} with AIDL HAL implementation + */ +@RunWith(MockitoJUnitRunner.class) +public final class IRadioServiceAidlImplTest { + + private static final int[] ENABLE_TYPES = new int[]{Announcement.TYPE_TRAFFIC}; + + private IRadioServiceAidlImpl mAidlImpl; + + @Mock + private BroadcastRadioService mServiceMock; + @Mock + private BroadcastRadioServiceImpl mHalMock; + @Mock + private RadioManager.ModuleProperties mModuleMock; + @Mock + private RadioManager.BandConfig mBandConfigMock; + @Mock + private ITunerCallback mTunerCallbackMock; + @Mock + private IAnnouncementListener mListenerMock; + @Mock + private ICloseHandle mICloseHandle; + @Mock + private ITuner mTunerMock; + + @Before + public void setUp() throws Exception { + doNothing().when(mServiceMock).enforcePolicyAccess(); + + when(mHalMock.listModules()).thenReturn(Arrays.asList(mModuleMock)); + when(mHalMock.openSession(anyInt(), any(), anyBoolean(), any())) + .thenReturn(mTunerMock); + when(mHalMock.addAnnouncementListener(any(), any())).thenReturn(mICloseHandle); + + mAidlImpl = new IRadioServiceAidlImpl(mServiceMock, mHalMock); + } + + @Test + public void loadModules_forAidlImpl() { + assertWithMessage("Modules loaded in AIDL HAL") + .that(mAidlImpl.listModules()) + .containsExactly(mModuleMock); + } + + @Test + public void openTuner_forAidlImpl() throws Exception { + ITuner tuner = mAidlImpl.openTuner(/* moduleId= */ 0, mBandConfigMock, + /* withAudio= */ true, mTunerCallbackMock); + + assertWithMessage("Tuner opened in AIDL HAL") + .that(tuner).isEqualTo(mTunerMock); + } + + @Test + public void addAnnouncementListener_forAidlImpl() { + ICloseHandle closeHandle = mAidlImpl.addAnnouncementListener(ENABLE_TYPES, mListenerMock); + + verify(mHalMock).addAnnouncementListener(ENABLE_TYPES, mListenerMock); + assertWithMessage("Close handle of announcement listener for HAL 2") + .that(closeHandle).isEqualTo(mICloseHandle); + } + +} diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java new file mode 100644 index 000000000000..f28e27d7f896 --- /dev/null +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.broadcastradio; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.radio.Announcement; +import android.hardware.radio.IAnnouncementListener; +import android.hardware.radio.ICloseHandle; +import android.hardware.radio.ITuner; +import android.hardware.radio.ITunerCallback; +import android.hardware.radio.RadioManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; + +/** + * Tests for {@link android.hardware.radio.IRadioService} with HIDL HAL implementation + */ +@RunWith(MockitoJUnitRunner.class) +public final class IRadioServiceHidlImplTest { + + private static final int HAL1_MODULE_ID = 0; + private static final int[] ENABLE_TYPES = new int[]{Announcement.TYPE_TRAFFIC}; + + private IRadioServiceHidlImpl mHidlImpl; + + @Mock + private BroadcastRadioService mServiceMock; + @Mock + private com.android.server.broadcastradio.hal1.BroadcastRadioService mHal1Mock; + @Mock + private com.android.server.broadcastradio.hal2.BroadcastRadioService mHal2Mock; + @Mock + private RadioManager.ModuleProperties mHal1ModuleMock; + @Mock + private RadioManager.ModuleProperties mHal2ModuleMock; + @Mock + private RadioManager.BandConfig mBandConfigMock; + @Mock + private ITunerCallback mTunerCallbackMock; + @Mock + private IAnnouncementListener mListenerMock; + @Mock + private ICloseHandle mICloseHandle; + @Mock + private ITuner mHal1TunerMock; + @Mock + private ITuner mHal2TunerMock; + + @Before + public void setup() throws Exception { + doNothing().when(mServiceMock).enforcePolicyAccess(); + when(mHal1Mock.loadModules()).thenReturn(Arrays.asList(mHal1ModuleMock)); + when(mHal1Mock.openTuner(anyInt(), any(), anyBoolean(), any())).thenReturn(mHal1TunerMock); + + when(mHal2Mock.listModules()).thenReturn(Arrays.asList(mHal2ModuleMock)); + doAnswer(invocation -> { + int moduleId = (int) invocation.getArguments()[0]; + return moduleId != HAL1_MODULE_ID; + }).when(mHal2Mock).hasModule(anyInt()); + when(mHal2Mock.openSession(anyInt(), any(), anyBoolean(), any())) + .thenReturn(mHal2TunerMock); + when(mHal2Mock.addAnnouncementListener(any(), any())).thenReturn(mICloseHandle); + + mHidlImpl = new IRadioServiceHidlImpl(mServiceMock, mHal1Mock, mHal2Mock); + } + + @Test + public void loadModules_forHidlImpl() { + assertWithMessage("Modules loaded in HIDL HAL") + .that(mHidlImpl.listModules()) + .containsExactly(mHal1ModuleMock, mHal2ModuleMock); + } + + @Test + public void openTuner_withHal1ModuleId_forHidlImpl() throws Exception { + ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID, mBandConfigMock, + /* withAudio= */ true, mTunerCallbackMock); + + assertWithMessage("Tuner opened in HAL 1") + .that(tuner).isEqualTo(mHal1TunerMock); + } + + @Test + public void openTuner_withHal2ModuleId_forHidlImpl() throws Exception { + ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID + 1, mBandConfigMock, + /* withAudio= */ true, mTunerCallbackMock); + + assertWithMessage("Tuner opened in HAL 2") + .that(tuner).isEqualTo(mHal2TunerMock); + } + + @Test + public void addAnnouncementListener_forHidlImpl() { + when(mHal2Mock.hasAnyModules()).thenReturn(true); + ICloseHandle closeHandle = mHidlImpl.addAnnouncementListener(ENABLE_TYPES, mListenerMock); + + verify(mHal2Mock).addAnnouncementListener(ENABLE_TYPES, mListenerMock); + assertWithMessage("Close handle of announcement listener for HAL 2") + .that(closeHandle).isEqualTo(mICloseHandle); + } + +} diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index 0b8b29b9dda9..bcb13d2108b8 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -48,6 +48,7 @@ import static com.android.internal.util.ContrastColorUtilTest.assertContrastIsWi import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.fail; import static org.junit.Assert.assertEquals; @@ -56,7 +57,9 @@ import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import android.annotation.Nullable; import android.app.Notification.CallStyle; @@ -68,6 +71,7 @@ import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; +import android.graphics.Typeface; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Build; @@ -79,7 +83,9 @@ import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; import android.text.style.TextAppearanceSpan; +import android.util.Pair; import android.widget.RemoteViews; import androidx.test.InstrumentationRegistry; @@ -89,6 +95,8 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; import com.android.internal.util.ContrastColorUtil; +import junit.framework.Assert; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -218,8 +226,10 @@ public class NotificationTest { @Test public void allPendingIntents_recollectedAfterReusingBuilder() { - PendingIntent intent1 = PendingIntent.getActivity(mContext, 0, new Intent("test1"), PendingIntent.FLAG_MUTABLE_UNAUDITED); - PendingIntent intent2 = PendingIntent.getActivity(mContext, 0, new Intent("test2"), PendingIntent.FLAG_MUTABLE_UNAUDITED); + PendingIntent intent1 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + PendingIntent intent2 = PendingIntent.getActivity( + mContext, 0, new Intent("test2"), PendingIntent.FLAG_IMMUTABLE); Notification.Builder builder = new Notification.Builder(mContext, "channel"); builder.setContentIntent(intent1); @@ -669,30 +679,23 @@ public class NotificationTest { Notification notification = new Notification.Builder(mContext, "Channel").setStyle( style).build(); + int targetSize = mContext.getResources().getDimensionPixelSize( + ActivityManager.isLowRamDeviceStatic() + ? R.dimen.notification_person_icon_max_size_low_ram + : R.dimen.notification_person_icon_max_size); + Bitmap personIcon = style.getUser().getIcon().getBitmap(); - assertThat(personIcon.getWidth()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); - assertThat(personIcon.getHeight()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); + assertThat(personIcon.getWidth()).isEqualTo(targetSize); + assertThat(personIcon.getHeight()).isEqualTo(targetSize); Bitmap avatarIcon = style.getMessages().get(0).getSenderPerson().getIcon().getBitmap(); - assertThat(avatarIcon.getWidth()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); - assertThat(avatarIcon.getHeight()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); + assertThat(avatarIcon.getWidth()).isEqualTo(targetSize); + assertThat(avatarIcon.getHeight()).isEqualTo(targetSize); Bitmap historicAvatarIcon = style.getHistoricMessages().get( 0).getSenderPerson().getIcon().getBitmap(); - assertThat(historicAvatarIcon.getWidth()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); - assertThat(historicAvatarIcon.getHeight()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); + assertThat(historicAvatarIcon.getWidth()).isEqualTo(targetSize); + assertThat(historicAvatarIcon.getHeight()).isEqualTo(targetSize); } @Test @@ -780,7 +783,6 @@ public class NotificationTest { assertFalse(notification.isMediaNotification()); } - @Test public void validateColorizedPaletteForColor(int rawColor) { Notification.Colors cDay = new Notification.Colors(); Notification.Colors cNight = new Notification.Colors(); @@ -861,19 +863,22 @@ public class NotificationTest { Bundle fakeTypes = new Bundle(); fakeTypes.putParcelable(EXTRA_LARGE_ICON_BIG, new Bundle()); - style.restoreFromExtras(fakeTypes); // no crash, good } @Test public void testRestoreFromExtras_Messaging_invalidExtra_noCrash() { - Notification.Style style = new Notification.MessagingStyle(); + Notification.Style style = new Notification.MessagingStyle("test"); Bundle fakeTypes = new Bundle(); fakeTypes.putParcelable(EXTRA_MESSAGING_PERSON, new Bundle()); fakeTypes.putParcelable(EXTRA_CONVERSATION_ICON, new Bundle()); - style.restoreFromExtras(fakeTypes); + Notification n = new Notification.Builder(mContext, "test") + .setStyle(style) + .setExtras(fakeTypes) + .build(); + Notification.Builder.recoverBuilder(mContext, n); // no crash, good } @@ -885,22 +890,33 @@ public class NotificationTest { fakeTypes.putParcelable(EXTRA_MEDIA_SESSION, new Bundle()); fakeTypes.putParcelable(EXTRA_MEDIA_REMOTE_INTENT, new Bundle()); - style.restoreFromExtras(fakeTypes); + Notification n = new Notification.Builder(mContext, "test") + .setStyle(style) + .setExtras(fakeTypes) + .build(); + Notification.Builder.recoverBuilder(mContext, n); // no crash, good } @Test public void testRestoreFromExtras_Call_invalidExtra_noCrash() { - Notification.Style style = new CallStyle(); + PendingIntent intent1 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + Notification.Style style = Notification.CallStyle.forIncomingCall( + new Person.Builder().setName("hi").build(), intent1, intent1); + Bundle fakeTypes = new Bundle(); fakeTypes.putParcelable(EXTRA_CALL_PERSON, new Bundle()); fakeTypes.putParcelable(EXTRA_ANSWER_INTENT, new Bundle()); fakeTypes.putParcelable(EXTRA_DECLINE_INTENT, new Bundle()); fakeTypes.putParcelable(EXTRA_HANG_UP_INTENT, new Bundle()); - style.restoreFromExtras(fakeTypes); - + Notification n = new Notification.Builder(mContext, "test") + .setStyle(style) + .setExtras(fakeTypes) + .build(); + Notification.Builder.recoverBuilder(mContext, n); // no crash, good } @@ -962,7 +978,11 @@ public class NotificationTest { fakeTypes.putParcelable(KEY_ON_READ, new Bundle()); fakeTypes.putParcelable(KEY_ON_REPLY, new Bundle()); fakeTypes.putParcelable(KEY_REMOTE_INPUT, new Bundle()); - Notification.CarExtender.UnreadConversation.getUnreadConversationFromBundle(fakeTypes); + + Notification n = new Notification.Builder(mContext, "test") + .setExtras(fakeTypes) + .build(); + Notification.CarExtender extender = new Notification.CarExtender(n); // no crash, good } @@ -980,6 +1000,493 @@ public class NotificationTest { // no crash, good } + + @Test + public void testDoesNotStripsExtenders() { + Notification.Builder nb = new Notification.Builder(mContext, "channel"); + nb.extend(new Notification.CarExtender().setColor(Color.RED)); + nb.extend(new Notification.TvExtender().setChannelId("different channel")); + nb.extend(new Notification.WearableExtender().setDismissalId("dismiss")); + Notification before = nb.build(); + Notification after = Notification.Builder.maybeCloneStrippedForDelivery(before); + + assertTrue(before == after); + + Assert.assertEquals("different channel", + new Notification.TvExtender(before).getChannelId()); + Assert.assertEquals(Color.RED, new Notification.CarExtender(before).getColor()); + Assert.assertEquals("dismiss", new Notification.WearableExtender(before).getDismissalId()); + } + + @Test + public void testStyleChangeVisiblyDifferent_noStyles() { + Notification.Builder n1 = new Notification.Builder(mContext, "test"); + Notification.Builder n2 = new Notification.Builder(mContext, "test"); + + assertFalse(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testStyleChangeVisiblyDifferent_noStyleToStyle() { + Notification.Builder n1 = new Notification.Builder(mContext, "test"); + Notification.Builder n2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle()); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testStyleChangeVisiblyDifferent_styleToNoStyle() { + Notification.Builder n2 = new Notification.Builder(mContext, "test"); + Notification.Builder n1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle()); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testStyleChangeVisiblyDifferent_changeStyle() { + Notification.Builder n1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.InboxStyle()); + Notification.Builder n2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle()); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testInboxTextChange() { + Notification.Builder nInbox1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.InboxStyle().addLine("a").addLine("b")); + Notification.Builder nInbox2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.InboxStyle().addLine("b").addLine("c")); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nInbox1, nInbox2)); + } + + @Test + public void testBigTextTextChange() { + Notification.Builder nBigText1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle().bigText("something")); + Notification.Builder nBigText2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle().bigText("else")); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigText1, nBigText2)); + } + + @Test + public void testBigPictureChange() { + Bitmap bitA = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); + Bitmap bitB = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888); + + Notification.Builder nBigPic1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigPictureStyle().bigPicture(bitA)); + Notification.Builder nBigPic2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigPictureStyle().bigPicture(bitB)); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigPic1, nBigPic2)); + } + + @Test + public void testMessagingChange_text() { + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build())) + .addMessage(new Notification.MessagingStyle.Message( + "b", 100, new Person.Builder().setName("hi").build())) + ); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_data() { + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()) + .setData("text", mock(Uri.class)))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()))); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_sender() { + Person a = new Person.Builder().setName("A").build(); + Person b = new Person.Builder().setName("b").build(); + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_key() { + Person a = new Person.Builder().setName("hi").setKey("A").build(); + Person b = new Person.Builder().setName("hi").setKey("b").build(); + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_ignoreTimeChange() { + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 1000, new Person.Builder().setName("hi").build())) + ); + + assertFalse(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testRemoteViews_nullChange() { + Notification.Builder n1 = new Notification.Builder(mContext, "test") + .setContent(mock(RemoteViews.class)); + Notification.Builder n2 = new Notification.Builder(mContext, "test"); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test"); + n2 = new Notification.Builder(mContext, "test") + .setContent(mock(RemoteViews.class)); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test") + .setCustomBigContentView(mock(RemoteViews.class)); + n2 = new Notification.Builder(mContext, "test"); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test"); + n2 = new Notification.Builder(mContext, "test") + .setCustomBigContentView(mock(RemoteViews.class)); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test"); + n2 = new Notification.Builder(mContext, "test"); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_layoutChange() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(189); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_layoutSame() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(234); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_sequenceChange() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + when(a.getSequenceNumber()).thenReturn(1); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(234); + when(b.getSequenceNumber()).thenReturn(2); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_sequenceSame() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + when(a.getSequenceNumber()).thenReturn(1); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(234); + when(b.getSequenceNumber()).thenReturn(1); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testActionsDifferent_null() { + Notification n1 = new Notification.Builder(mContext, "test") + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentSame() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentText() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) + .build(); + + assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentSpannables() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, + new SpannableStringBuilder().append("test1", + new StyleSpan(Typeface.BOLD), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE), + intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "test1", intent).build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentNumber() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) + .build(); + + assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentIntent() { + PendingIntent intent1 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + PendingIntent intent2 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent1).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent2).build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsIgnoresRemoteInputs() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(new RemoteInput.Builder("a") + .setChoices(new CharSequence[] {"i", "m"}) + .build()) + .build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(new RemoteInput.Builder("a") + .setChoices(new CharSequence[] {"t", "m"}) + .build()) + .build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testFreeformRemoteInputActionPair_noRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + Notification notification = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .build()) + .build(); + Assert.assertNull(notification.findRemoteInputActionPair(false)); + } + + @Test + public void testFreeformRemoteInputActionPair_hasRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + RemoteInput remoteInput = new RemoteInput.Builder("a").build(); + + Notification.Action actionWithRemoteInput = + new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(remoteInput) + .addRemoteInput(remoteInput) + .build(); + + Notification.Action actionWithoutRemoteInput = + new Notification.Action.Builder(icon, "TEXT 2", intent) + .build(); + + Notification notification = new Notification.Builder(mContext, "test") + .addAction(actionWithoutRemoteInput) + .addAction(actionWithRemoteInput) + .build(); + + Pair<RemoteInput, Notification.Action> remoteInputActionPair = + notification.findRemoteInputActionPair(false); + + assertNotNull(remoteInputActionPair); + Assert.assertEquals(remoteInput, remoteInputActionPair.first); + Assert.assertEquals(actionWithRemoteInput, remoteInputActionPair.second); + } + + @Test + public void testFreeformRemoteInputActionPair_requestFreeform_noFreeformRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + Notification notification = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput( + new RemoteInput.Builder("a") + .setAllowFreeFormInput(false).build()) + .build()) + .build(); + Assert.assertNull(notification.findRemoteInputActionPair(true)); + } + + @Test + public void testFreeformRemoteInputActionPair_requestFreeform_hasFreeformRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + RemoteInput remoteInput = + new RemoteInput.Builder("a").setAllowFreeFormInput(false).build(); + RemoteInput freeformRemoteInput = + new RemoteInput.Builder("b").setAllowFreeFormInput(true).build(); + + Notification.Action actionWithFreeformRemoteInput = + new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(remoteInput) + .addRemoteInput(freeformRemoteInput) + .build(); + + Notification.Action actionWithoutFreeformRemoteInput = + new Notification.Action.Builder(icon, "TEXT 2", intent) + .addRemoteInput(remoteInput) + .build(); + + Notification notification = new Notification.Builder(mContext, "test") + .addAction(actionWithoutFreeformRemoteInput) + .addAction(actionWithFreeformRemoteInput) + .build(); + + Pair<RemoteInput, Notification.Action> remoteInputActionPair = + notification.findRemoteInputActionPair(true); + + assertNotNull(remoteInputActionPair); + Assert.assertEquals(freeformRemoteInput, remoteInputActionPair.first); + Assert.assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second); + } + private void assertValid(Notification.Colors c) { // Assert that all colors are populated assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID); diff --git a/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java b/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java deleted file mode 100644 index 7c7cd12bcb73..000000000000 --- a/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.app.time; - -import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; - -import static com.google.common.truth.Truth.assertThat; - -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class TimeConfigurationTest { - - @Test - public void testBuilder() { - TimeConfiguration first = new TimeConfiguration.Builder() - .setAutoDetectionEnabled(true) - .build(); - - assertThat(first.isAutoDetectionEnabled()).isTrue(); - - TimeConfiguration copyFromBuilderConfiguration = new TimeConfiguration.Builder(first) - .build(); - - assertThat(first).isEqualTo(copyFromBuilderConfiguration); - } - - @Test - public void testParcelable() { - TimeConfiguration.Builder builder = new TimeConfiguration.Builder(); - - assertRoundTripParcelable(builder.setAutoDetectionEnabled(true).build()); - - assertRoundTripParcelable(builder.setAutoDetectionEnabled(false).build()); - } - -} diff --git a/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java b/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java index 3ab01f3d8832..e7d352cfee30 100644 --- a/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java +++ b/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java @@ -16,11 +16,9 @@ package android.app.time; -import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; import android.os.ShellCommand; @@ -31,35 +29,12 @@ import org.junit.runner.RunWith; /** * Tests for non-SDK methods on {@link UnixEpochTime}. + * + * <p>See also {@link android.app.time.cts.UnixEpochTimeTest} for SDK methods. */ @RunWith(AndroidJUnit4.class) public class UnixEpochTimeTest { - @Test - public void testEqualsAndHashcode() { - UnixEpochTime one1000one = new UnixEpochTime(1000, 1); - assertEqualsAndHashCode(one1000one, one1000one); - - UnixEpochTime one1000two = new UnixEpochTime(1000, 1); - assertEqualsAndHashCode(one1000one, one1000two); - - UnixEpochTime two1000 = new UnixEpochTime(1000, 2); - assertNotEquals(one1000one, two1000); - - UnixEpochTime one2000 = new UnixEpochTime(2000, 1); - assertNotEquals(one1000one, one2000); - } - - private static void assertEqualsAndHashCode(Object one, Object two) { - assertEquals(one, two); - assertEquals(one.hashCode(), two.hashCode()); - } - - @Test - public void testParceling() { - assertRoundTripParcelable(new UnixEpochTime(1000, 1)); - } - @Test(expected = IllegalArgumentException.class) public void testParseCommandLineArg_noElapsedRealtime() { ShellCommand testShellCommand = createShellCommandWithArgsAndOptions( @@ -91,22 +66,6 @@ public class UnixEpochTimeTest { } @Test - public void testAt() { - long timeMillis = 1000L; - int elapsedRealtimeMillis = 100; - UnixEpochTime unixEpochTime = new UnixEpochTime(elapsedRealtimeMillis, timeMillis); - // Reference time is after the timestamp. - UnixEpochTime at125 = unixEpochTime.at(125); - assertEquals(timeMillis + (125 - elapsedRealtimeMillis), at125.getUnixEpochTimeMillis()); - assertEquals(125, at125.getElapsedRealtimeMillis()); - - // Reference time is before the timestamp. - UnixEpochTime at75 = unixEpochTime.at(75); - assertEquals(timeMillis + (75 - elapsedRealtimeMillis), at75.getUnixEpochTimeMillis()); - assertEquals(75, at75.getElapsedRealtimeMillis()); - } - - @Test public void testElapsedRealtimeDifference() { UnixEpochTime value1 = new UnixEpochTime(1000, 123L); assertEquals(0, UnixEpochTime.elapsedRealtimeDifference(value1, value1)); diff --git a/graphics/java/android/graphics/BLASTBufferQueue.java b/graphics/java/android/graphics/BLASTBufferQueue.java index 1c41d06a3da2..9940ca3933c8 100644 --- a/graphics/java/android/graphics/BLASTBufferQueue.java +++ b/graphics/java/android/graphics/BLASTBufferQueue.java @@ -47,7 +47,7 @@ public final class BLASTBufferQueue { TransactionHangCallback callback); public interface TransactionHangCallback { - void onTransactionHang(boolean isGpuHang); + void onTransactionHang(String reason); } /** Create a new connection with the surface flinger. */ diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 0bc70857a113..3ee20ea95ee5 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -321,4 +321,21 @@ <!-- The smaller size of the dismiss target (shrinks when something is in the target). --> <dimen name="floating_dismiss_circle_small">120dp</dimen> + + <!-- The thickness of shadows of a window that has focus in DIP. --> + <dimen name="freeform_decor_shadow_focused_thickness">20dp</dimen> + + <!-- The thickness of shadows of a window that doesn't have focus in DIP. --> + <dimen name="freeform_decor_shadow_unfocused_thickness">5dp</dimen> + + <!-- Height of button (32dp) + 2 * margin (5dp each). --> + <dimen name="freeform_decor_caption_height">42dp</dimen> + + <!-- Width of buttons (64dp) + handle (128dp) + padding (24dp total). --> + <dimen name="freeform_decor_caption_width">216dp</dimen> + + <dimen name="freeform_resize_handle">30dp</dimen> + + <dimen name="freeform_resize_corner">44dp</dimen> + </resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java index e90389764af3..f209521b1da4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java @@ -33,6 +33,8 @@ public class SplitBounds implements Parcelable { // This class is orientation-agnostic, so we compute both for later use public final float topTaskPercent; public final float leftTaskPercent; + public final float dividerWidthPercent; + public final float dividerHeightPercent; /** * If {@code true}, that means at the time of creation of this object, the * split-screened apps were vertically stacked. This is useful in scenarios like @@ -62,8 +64,12 @@ public class SplitBounds implements Parcelable { appsStackedVertically = false; } - leftTaskPercent = this.leftTopBounds.width() / (float) rightBottomBounds.right; - topTaskPercent = this.leftTopBounds.height() / (float) rightBottomBounds.bottom; + float totalWidth = rightBottomBounds.right - leftTopBounds.left; + float totalHeight = rightBottomBounds.bottom - leftTopBounds.top; + leftTaskPercent = leftTopBounds.width() / totalWidth; + topTaskPercent = leftTopBounds.height() / totalHeight; + dividerWidthPercent = visualDividerBounds.width() / totalWidth; + dividerHeightPercent = visualDividerBounds.height() / totalHeight; } public SplitBounds(Parcel parcel) { @@ -75,6 +81,8 @@ public class SplitBounds implements Parcelable { appsStackedVertically = parcel.readBoolean(); leftTopTaskId = parcel.readInt(); rightBottomTaskId = parcel.readInt(); + dividerWidthPercent = parcel.readInt(); + dividerHeightPercent = parcel.readInt(); } @Override @@ -87,6 +95,8 @@ public class SplitBounds implements Parcelable { parcel.writeBoolean(appsStackedVertically); parcel.writeInt(leftTopTaskId); parcel.writeInt(rightBottomTaskId); + parcel.writeFloat(dividerWidthPercent); + parcel.writeFloat(dividerHeightPercent); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 87700ee4fb50..9d61c14e1435 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -21,7 +21,6 @@ import android.app.WindowConfiguration; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Color; -import android.graphics.Rect; import android.graphics.drawable.VectorDrawable; import android.os.Handler; import android.view.Choreographer; @@ -43,22 +42,6 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus; * The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't. */ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { - // The thickness of shadows of a window that has focus in DIP. - private static final int DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP = 20; - // The thickness of shadows of a window that doesn't have focus in DIP. - private static final int DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP = 5; - - // Height of button (32dp) + 2 * margin (5dp each) - private static final int DECOR_CAPTION_HEIGHT_IN_DIP = 42; - // Width of buttons (64dp) + handle (128dp) + padding (24dp total) - private static final int DECOR_CAPTION_WIDTH_IN_DIP = 216; - private static final int RESIZE_HANDLE_IN_DIP = 30; - private static final int RESIZE_CORNER_IN_DIP = 44; - - private static final Rect EMPTY_OUTSET = new Rect(); - private static final Rect RESIZE_HANDLE_OUTSET = new Rect( - RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP); - private final Handler mHandler; private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; @@ -69,6 +52,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL private DragResizeInputListener mDragResizeListener; + private RelayoutParams mRelayoutParams = new RelayoutParams(); private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult = new WindowDecoration.RelayoutResult<>(); @@ -114,19 +98,32 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL void relayout(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - final int shadowRadiusDp = taskInfo.isFocused - ? DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP : DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP; - final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode() - == WindowConfiguration.WINDOWING_MODE_FREEFORM; - final boolean isDragResizeable = isFreeform && mTaskInfo.isResizeable; - final Rect outset = isDragResizeable ? RESIZE_HANDLE_OUTSET : EMPTY_OUTSET; + final int shadowRadiusID = taskInfo.isFocused + ? R.dimen.freeform_decor_shadow_focused_thickness + : R.dimen.freeform_decor_shadow_unfocused_thickness; + final boolean isFreeform = + taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; + final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); - relayout(taskInfo, R.layout.caption_window_decoration, oldRootView, - DECOR_CAPTION_HEIGHT_IN_DIP, DECOR_CAPTION_WIDTH_IN_DIP, outset, shadowRadiusDp, - startT, finishT, wct, mResult); + + int outsetLeftId = R.dimen.freeform_resize_handle; + int outsetTopId = R.dimen.freeform_resize_handle; + int outsetRightId = R.dimen.freeform_resize_handle; + int outsetBottomId = R.dimen.freeform_resize_handle; + + mRelayoutParams.reset(); + mRelayoutParams.mRunningTaskInfo = taskInfo; + mRelayoutParams.mLayoutResId = R.layout.caption_window_decoration; + mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height; + mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width; + mRelayoutParams.mShadowRadiusId = shadowRadiusID; + if (isDragResizeable) { + mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId); + } + relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); mTaskOrganizer.applyTransaction(wct); @@ -167,10 +164,12 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL } int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()).getScaledTouchSlop(); - + int resize_handle = mResult.mRootView.getResources() + .getDimensionPixelSize(R.dimen.freeform_resize_handle); + int resize_corner = mResult.mRootView.getResources() + .getDimensionPixelSize(R.dimen.freeform_resize_corner); mDragResizeListener.setGeometry( - mResult.mWidth, mResult.mHeight, (int) (mResult.mDensity * RESIZE_HANDLE_IN_DIP), - (int) (mResult.mDensity * RESIZE_CORNER_IN_DIP), touchSlop); + mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index bf863ea2c7ab..b314163802ca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -19,11 +19,11 @@ package com.android.wm.shell.windowdecor; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; -import android.util.DisplayMetrics; import android.view.Display; import android.view.InsetsState; import android.view.LayoutInflater; @@ -91,7 +91,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> SurfaceControl mTaskBackgroundSurface; SurfaceControl mCaptionContainerSurface; - private CaptionWindowManager mCaptionWindowManager; + private WindowlessWindowManager mCaptionWindowManager; private SurfaceControlViewHost mViewHost; private final Rect mCaptionInsetsRect = new Rect(); @@ -142,15 +142,14 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> */ abstract void relayout(RunningTaskInfo taskInfo); - void relayout(RunningTaskInfo taskInfo, int layoutResId, T rootView, float captionHeightDp, - float captionWidthDp, Rect outsetsDp, float shadowRadiusDp, - SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - WindowContainerTransaction wct, RelayoutResult<T> outResult) { + void relayout(RelayoutParams params, SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, WindowContainerTransaction wct, T rootView, + RelayoutResult<T> outResult) { outResult.reset(); final Configuration oldTaskConfig = mTaskInfo.getConfiguration(); - if (taskInfo != null) { - mTaskInfo = taskInfo; + if (params.mRunningTaskInfo != null) { + mTaskInfo = params.mRunningTaskInfo; } if (!mTaskInfo.isVisible) { @@ -159,7 +158,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return; } - if (rootView == null && layoutResId == 0) { + if (rootView == null && params.mLayoutResId == 0) { throw new IllegalArgumentException("layoutResId and rootView can't both be invalid."); } @@ -176,15 +175,15 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return; } mDecorWindowContext = mContext.createConfigurationContext(taskConfig); - if (layoutResId != 0) { - outResult.mRootView = - (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null); + if (params.mLayoutResId != 0) { + outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext) + .inflate(params.mLayoutResId, null); } } if (outResult.mRootView == null) { - outResult.mRootView = - (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null); + outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext) + .inflate(params.mLayoutResId , null); } // DecorationContainerSurface @@ -200,18 +199,19 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); - outResult.mDensity = taskConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE; - final int decorContainerOffsetX = -(int) (outsetsDp.left * outResult.mDensity); - final int decorContainerOffsetY = -(int) (outsetsDp.top * outResult.mDensity); + final Resources resources = mDecorWindowContext.getResources(); + final int decorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId); + final int decorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId); outResult.mWidth = taskBounds.width() - + (int) (outsetsDp.right * outResult.mDensity) + + loadDimensionPixelSize(resources, params.mOutsetRightId) - decorContainerOffsetX; outResult.mHeight = taskBounds.height() - + (int) (outsetsDp.bottom * outResult.mDensity) + + loadDimensionPixelSize(resources, params.mOutsetBottomId) - decorContainerOffsetY; startT.setPosition( mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY) - .setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight) + .setWindowCrop(mDecorationContainerSurface, + outResult.mWidth, outResult.mHeight) // TODO(b/244455401): Change the z-order when it's better organized .setLayer(mDecorationContainerSurface, mTaskInfo.numActivities + 1) .show(mDecorationContainerSurface); @@ -226,12 +226,13 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .build(); } - float shadowRadius = outResult.mDensity * shadowRadiusDp; + float shadowRadius = loadDimension(resources, params.mShadowRadiusId); int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor(); mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f; mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f; mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f; - startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), taskBounds.height()) + startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), + taskBounds.height()) .setShadowRadius(mTaskBackgroundSurface, shadowRadius) .setColor(mTaskBackgroundSurface, mTmpColor) // TODO(b/244455401): Change the z-order when it's better organized @@ -248,8 +249,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .build(); } - final int captionHeight = (int) Math.ceil(captionHeightDp * outResult.mDensity); - final int captionWidth = (int) Math.ceil(captionWidthDp * outResult.mDensity); + final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); + final int captionWidth = loadDimensionPixelSize(resources, params.mCaptionWidthId); //Prevent caption from going offscreen if task is too high up final int captionYPos = taskBounds.top <= captionHeight / 2 ? 0 : captionHeight / 2; @@ -264,8 +265,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> if (mCaptionWindowManager == null) { // Put caption under a container surface because ViewRootImpl sets the destination frame // of windowless window layers and BLASTBufferQueue#update() doesn't support offset. - mCaptionWindowManager = new CaptionWindowManager( - mTaskInfo.getConfiguration(), mCaptionContainerSurface); + mCaptionWindowManager = new WindowlessWindowManager( + mTaskInfo.getConfiguration(), mCaptionContainerSurface, + null /* hostInputToken */); } // Caption view @@ -289,8 +291,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> // Caption insets mCaptionInsetsRect.set(taskBounds); - mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + captionHeight - captionYPos; - wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, CAPTION_INSETS_TYPES); + mCaptionInsetsRect.bottom = + mCaptionInsetsRect.top + captionHeight - captionYPos; + wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, + CAPTION_INSETS_TYPES); } else { startT.hide(mCaptionContainerSurface); } @@ -365,34 +369,67 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> releaseViews(); } + private static int loadDimensionPixelSize(Resources resources, int resourceId) { + if (resourceId == Resources.ID_NULL) { + return 0; + } + return resources.getDimensionPixelSize(resourceId); + } + + private static float loadDimension(Resources resources, int resourceId) { + if (resourceId == Resources.ID_NULL) { + return 0; + } + return resources.getDimension(resourceId); + } + + static class RelayoutParams{ + RunningTaskInfo mRunningTaskInfo; + int mLayoutResId; + int mCaptionHeightId; + int mCaptionWidthId; + int mShadowRadiusId; + + int mOutsetTopId; + int mOutsetBottomId; + int mOutsetLeftId; + int mOutsetRightId; + + void setOutsets(int leftId, int topId, int rightId, int bottomId) { + mOutsetLeftId = leftId; + mOutsetTopId = topId; + mOutsetRightId = rightId; + mOutsetBottomId = bottomId; + } + + void reset() { + mLayoutResId = Resources.ID_NULL; + mCaptionHeightId = Resources.ID_NULL; + mCaptionWidthId = Resources.ID_NULL; + mShadowRadiusId = Resources.ID_NULL; + + mOutsetTopId = Resources.ID_NULL; + mOutsetBottomId = Resources.ID_NULL; + mOutsetLeftId = Resources.ID_NULL; + mOutsetRightId = Resources.ID_NULL; + } + } + static class RelayoutResult<T extends View & TaskFocusStateConsumer> { int mWidth; int mHeight; - float mDensity; T mRootView; void reset() { mWidth = 0; mHeight = 0; - mDensity = 0; mRootView = null; } } - private static class CaptionWindowManager extends WindowlessWindowManager { - CaptionWindowManager(Configuration config, SurfaceControl rootSurface) { - super(config, rootSurface, null /* hostInputToken */); - } - - @Override - public void setConfiguration(Configuration configuration) { - super.setConfiguration(configuration); - } - } - interface SurfaceControlViewHostFactory { default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { return new SurfaceControlViewHost(c, d, wmm); } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt index fa783f231607..45eae2e2fe40 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt @@ -26,7 +26,6 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowBecomesInvisible import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd @@ -35,7 +34,6 @@ import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible import com.android.wm.shell.flicker.splitScreenDismissed import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible -import org.junit.Assume import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -95,7 +93,9 @@ class DismissSplitScreenByDivider (testSpec: FlickerTestParameter) : SplitScreen fun primaryAppBoundsBecomesInvisible() = testSpec.splitAppLayerBoundsBecomesInvisible( primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false) - private fun secondaryAppBoundsIsFullscreenAtEnd_internal() { + @Presubmit + @Test + fun secondaryAppBoundsIsFullscreenAtEnd() { testSpec.assertLayers { this.isVisible(secondaryApp) .isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) @@ -117,20 +117,6 @@ class DismissSplitScreenByDivider (testSpec: FlickerTestParameter) : SplitScreen @Presubmit @Test - fun secondaryAppBoundsIsFullscreenAtEnd() { - Assume.assumeFalse(isShellTransitionsEnabled) - secondaryAppBoundsIsFullscreenAtEnd_internal() - } - - @FlakyTest(bugId = 250528485) - @Test - fun secondaryAppBoundsIsFullscreenAtEnd_shellTransit() { - Assume.assumeTrue(isShellTransitionsEnabled) - secondaryAppBoundsIsFullscreenAtEnd_internal() - } - - @Presubmit - @Test fun primaryAppWindowBecomesInvisible() = testSpec.appWindowBecomesInvisible(primaryApp) @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt index 84a8c0a59f32..73159c981b82 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.splitscreen -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.IwTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit @@ -146,19 +145,15 @@ class SwitchAppByDoubleTapDivider(testSpec: FlickerTestParameter) : SplitScreenB // robust enough to get the correct end state. } - @FlakyTest(bugId = 241524174) @Test fun splitScreenDividerKeepVisible() = testSpec.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) - @FlakyTest(bugId = 241524174) @Test fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp) - @FlakyTest(bugId = 241524174) @Test fun secondaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(secondaryApp) - @FlakyTest(bugId = 241524174) @Test fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( primaryApp, @@ -166,9 +161,6 @@ class SwitchAppByDoubleTapDivider(testSpec: FlickerTestParameter) : SplitScreenB portraitPosTop = true ) - // TODO(b/246490534): Move back to presubmit after withAppTransitionIdle is robust enough to - // get the correct end state. - @FlakyTest(bugId = 246490534) @Test fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( secondaryApp, @@ -176,11 +168,9 @@ class SwitchAppByDoubleTapDivider(testSpec: FlickerTestParameter) : SplitScreenB portraitPosTop = false ) - @FlakyTest(bugId = 241524174) @Test fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp) - @FlakyTest(bugId = 241524174) @Test fun secondaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(secondaryApp) diff --git a/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml new file mode 100644 index 000000000000..8949a75d1a15 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <!-- Resources used in WindowDecorationTests --> + <dimen name="test_freeform_decor_caption_height">32dp</dimen> + <dimen name="test_freeform_decor_caption_width">216dp</dimen> + <dimen name="test_window_decor_left_outset">10dp</dimen> + <dimen name="test_window_decor_top_outset">20dp</dimen> + <dimen name="test_window_decor_right_outset">30dp</dimen> + <dimen name="test_window_decor_bottom_outset">40dp</dimen> + <dimen name="test_window_decor_shadow_radius">5dp</dimen> + <dimen name="test_window_decor_resize_handle">10dp</dimen> +</resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index fa62b9c00fc7..4d37e5dbc4dc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -50,11 +50,13 @@ import android.view.WindowManager.LayoutParams; import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.tests.R; import org.junit.Before; import org.junit.Test; @@ -76,13 +78,9 @@ import java.util.function.Supplier; @SmallTest @RunWith(AndroidTestingRunner.class) public class WindowDecorationTests extends ShellTestCase { - private static final int CAPTION_HEIGHT_DP = 32; - private static final int CAPTION_WIDTH_DP = 216; - private static final int SHADOW_RADIUS_DP = 5; private static final Rect TASK_BOUNDS = new Rect(100, 300, 400, 400); private static final Point TASK_POSITION_IN_PARENT = new Point(40, 60); - private final Rect mOutsetsDp = new Rect(); private final WindowDecoration.RelayoutResult<TestView> mRelayoutResult = new WindowDecoration.RelayoutResult<>(); @@ -104,6 +102,7 @@ public class WindowDecorationTests extends ShellTestCase { private final List<SurfaceControl.Builder> mMockSurfaceControlBuilders = new ArrayList<>(); private SurfaceControl.Transaction mMockSurfaceControlStartT; private SurfaceControl.Transaction mMockSurfaceControlFinishT; + private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams(); @Before public void setUp() { @@ -147,7 +146,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mOutsetsDp.set(10, 20, 30, 40); + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -197,8 +200,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mOutsetsDp.set(10, 20, 30, 40); - + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -226,16 +232,17 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockSurfaceControlStartT).show(captionContainerSurface); verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any()); + verify(mMockSurfaceControlViewHost) .setView(same(mMockView), argThat(lp -> lp.height == 64 - && lp.width == 300 + && lp.width == 432 && (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0)); if (ViewRootImpl.CAPTION_ON_SHELL) { verify(mMockView).setTaskFocusState(true); verify(mMockWindowContainerTransaction) .addRectInsetsProvider(taskInfo.token, - new Rect(100, 300, 400, 364), + new Rect(100, 300, 400, 332), new int[] { InsetsState.ITYPE_CAPTION_BAR }); } @@ -248,7 +255,6 @@ public class WindowDecorationTests extends ShellTestCase { assertEquals(380, mRelayoutResult.mWidth); assertEquals(220, mRelayoutResult.mHeight); - assertEquals(2, mRelayoutResult.mDensity, 0.f); } @Test @@ -287,7 +293,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mOutsetsDp.set(10, 20, 30, 40); + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -358,7 +368,8 @@ public class WindowDecorationTests extends ShellTestCase { private TestWindowDecoration createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) { - return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer, + return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(), + mMockDisplayController, mMockShellTaskOrganizer, taskInfo, testSurface, new MockObjectSupplier<>(mMockSurfaceControlBuilders, () -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))), @@ -410,9 +421,13 @@ public class WindowDecorationTests extends ShellTestCase { @Override void relayout(ActivityManager.RunningTaskInfo taskInfo) { - relayout(null /* taskInfo */, 0 /* layoutResId */, mMockView, CAPTION_HEIGHT_DP, - CAPTION_WIDTH_DP, mOutsetsDp, SHADOW_RADIUS_DP, mMockSurfaceControlStartT, - mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mRelayoutResult); + mRelayoutParams.mLayoutResId = 0; + mRelayoutParams.mCaptionHeightId = R.dimen.test_freeform_decor_caption_height; + mRelayoutParams.mCaptionWidthId = R.dimen.test_freeform_decor_caption_width; + mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius; + + relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT, + mMockWindowContainerTransaction, mMockView, mRelayoutResult); } } } diff --git a/media/java/android/media/ImageWriter.java b/media/java/android/media/ImageWriter.java index 39b3d0b47a27..0291f64c0640 100644 --- a/media/java/android/media/ImageWriter.java +++ b/media/java/android/media/ImageWriter.java @@ -264,10 +264,9 @@ public class ImageWriter implements AutoCloseable { if (useSurfaceImageFormatInfo) { // nativeInit internally overrides UNKNOWN format. So does surface format query after // nativeInit and before getEstimatedNativeAllocBytes(). - imageFormat = SurfaceUtils.getSurfaceFormat(surface); - mDataSpace = dataSpace = PublicFormatUtils.getHalDataspace(imageFormat); - mHardwareBufferFormat = - hardwareBufferFormat = PublicFormatUtils.getHalFormat(imageFormat); + mHardwareBufferFormat = hardwareBufferFormat = SurfaceUtils.getSurfaceFormat(surface); + mDataSpace = dataSpace = SurfaceUtils.getSurfaceDataspace(surface); + imageFormat = PublicFormatUtils.getPublicFormat(hardwareBufferFormat, dataSpace); } // Estimate the native buffer allocation size and register it so it gets accounted for diff --git a/media/java/android/media/MediaCrypto.java b/media/java/android/media/MediaCrypto.java index 889a5f7efbb2..1930262ea253 100644 --- a/media/java/android/media/MediaCrypto.java +++ b/media/java/android/media/MediaCrypto.java @@ -75,14 +75,17 @@ public final class MediaCrypto { public final native boolean requiresSecureDecoderComponent(@NonNull String mime); /** - * Associate a MediaDrm session with this MediaCrypto instance. The - * MediaDrm session is used to securely load decryption keys for a - * crypto scheme. The crypto keys loaded through the MediaDrm session + * Associate a new MediaDrm session with this MediaCrypto instance. + * + * <p>The MediaDrm session is used to securely load decryption keys for a + * crypto scheme. The crypto keys loaded through the MediaDrm session * may be selected for use during the decryption operation performed * by {@link android.media.MediaCodec#queueSecureInputBuffer} by specifying - * their key ids in the {@link android.media.MediaCodec.CryptoInfo#key} field. - * @param sessionId the MediaDrm sessionId to associate with this - * MediaCrypto instance + * their key IDs in the {@link android.media.MediaCodec.CryptoInfo#key} field. + * + * @param sessionId The MediaDrm sessionId to associate with this MediaCrypto + * instance. The session's scheme must match the scheme UUID used when + * constructing this MediaCrypto instance. * @throws MediaCryptoException on failure to set the sessionId */ public final native void setMediaDrmSession(@NonNull byte[] sessionId) diff --git a/packages/CarrierDefaultApp/Android.bp b/packages/CarrierDefaultApp/Android.bp index fc753da19394..6990ad0fbd7d 100644 --- a/packages/CarrierDefaultApp/Android.bp +++ b/packages/CarrierDefaultApp/Android.bp @@ -10,6 +10,7 @@ package { android_app { name: "CarrierDefaultApp", srcs: ["src/**/*.java"], + libs: ["SliceStore"], platform_apis: true, certificate: "platform", } diff --git a/packages/CarrierDefaultApp/AndroidManifest.xml b/packages/CarrierDefaultApp/AndroidManifest.xml index 632dfb397d51..a5b104b597ee 100644 --- a/packages/CarrierDefaultApp/AndroidManifest.xml +++ b/packages/CarrierDefaultApp/AndroidManifest.xml @@ -28,6 +28,7 @@ <uses-permission android:name="android.permission.NETWORK_BYPASS_PRIVATE_DNS" /> <uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> <application android:label="@string/app_name" @@ -71,5 +72,22 @@ <data android:host="*" /> </intent-filter> </activity-alias> + + <receiver android:name="com.android.carrierdefaultapp.SliceStoreBroadcastReceiver" + android:exported="true"> + <intent-filter> + <action android:name="com.android.phone.slicestore.action.START_SLICE_STORE" /> + <action android:name="com.android.phone.slicestore.action.SLICE_STORE_RESPONSE_TIMEOUT" /> + <action android:name="com.android.phone.slicestore.action.NOTIFICATION_CANCELED" /> + </intent-filter> + </receiver> + <activity android:name="com.android.carrierdefaultapp.SliceStoreActivity" + android:label="@string/slice_store_label" + android:exported="true" + android:configChanges="keyboardHidden|orientation|screenSize"> + <intent-filter> + <category android:name="android.intent.category.DEFAULT"/> + </intent-filter> + </activity> </application> </manifest> diff --git a/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml b/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml new file mode 100644 index 000000000000..ad8a21c7abb6 --- /dev/null +++ b/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml @@ -0,0 +1,23 @@ +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="@android:color/white" + android:pathData="M3,17V15H8Q8,15 8,15Q8,15 8,15V13Q8,13 8,13Q8,13 8,13H3V7H10V9H5V11H8Q8.825,11 9.413,11.587Q10,12.175 10,13V15Q10,15.825 9.413,16.413Q8.825,17 8,17ZM21,11V15Q21,15.825 20.413,16.413Q19.825,17 19,17H14Q13.175,17 12.588,16.413Q12,15.825 12,15V9Q12,8.175 12.588,7.587Q13.175,7 14,7H19Q19.825,7 20.413,7.587Q21,8.175 21,9H14Q14,9 14,9Q14,9 14,9V15Q14,15 14,15Q14,15 14,15H19Q19,15 19,15Q19,15 19,15V13H16.5V11Z"/> +</vector>
\ No newline at end of file diff --git a/packages/CarrierDefaultApp/res/values/strings.xml b/packages/CarrierDefaultApp/res/values/strings.xml index 65a7cecca20f..ce88a401d18e 100644 --- a/packages/CarrierDefaultApp/res/values/strings.xml +++ b/packages/CarrierDefaultApp/res/values/strings.xml @@ -13,4 +13,18 @@ <string name="ssl_error_warning">The network you’re trying to join has security issues.</string> <string name="ssl_error_example">For example, the login page may not belong to the organization shown.</string> <string name="ssl_error_continue">Continue anyway via browser</string> + + <!-- Telephony notification channel name for network boost notifications. --> + <string name="network_boost_notification_channel">Network Boost</string> + <!-- Notification title text for the network boost notification. --> + <string name="network_boost_notification_title">%s recommends a data boost</string> + <!-- Notification detail text for the network boost notification. --> + <string name="network_boost_notification_detail">Buy a network boost for better performance</string> + <!-- Notification button text to cancel the network boost notification. --> + <string name="network_boost_notification_button_not_now">Not now</string> + <!-- Notification button text to manage the network boost notification. --> + <string name="network_boost_notification_button_manage">Manage</string> + + <!-- Label to display when the slice store opens. --> + <string name="slice_store_label">Purchase a network boost.</string> </resources> diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java new file mode 100644 index 000000000000..24cb5f948474 --- /dev/null +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.carrierdefaultapp; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Activity; +import android.app.NotificationManager; +import android.content.Intent; +import android.os.Bundle; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.webkit.WebView; + +import com.android.phone.slicestore.SliceStore; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Activity that launches when the user clicks on the network boost notification. + */ +public class SliceStoreActivity extends Activity { + private static final String TAG = "SliceStoreActivity"; + + private URL mUrl; + private WebView mWebView; + private int mPhoneId; + private int mSubId; + private @TelephonyManager.PremiumCapability int mCapability; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + mPhoneId = intent.getIntExtra(SliceStore.EXTRA_PHONE_ID, + SubscriptionManager.INVALID_PHONE_INDEX); + mSubId = intent.getIntExtra(SliceStore.EXTRA_SUB_ID, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + mCapability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY, + SliceStore.PREMIUM_CAPABILITY_INVALID); + mUrl = getUrl(); + logd("onCreate: mPhoneId=" + mPhoneId + ", mSubId=" + mSubId + ", mCapability=" + + TelephonyManager.convertPremiumCapabilityToString(mCapability) + + ", mUrl=" + mUrl); + getApplicationContext().getSystemService(NotificationManager.class) + .cancel(SliceStoreBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG, mCapability); + if (!SliceStoreBroadcastReceiver.isIntentValid(intent)) { + loge("Not starting SliceStoreActivity with an invalid Intent: " + intent); + SliceStoreBroadcastReceiver.sendSliceStoreResponse( + intent, SliceStore.EXTRA_INTENT_REQUEST_FAILED); + finishAndRemoveTask(); + return; + } + if (mUrl == null) { + loge("Unable to create a URL from carrier configs."); + SliceStoreBroadcastReceiver.sendSliceStoreResponse( + intent, SliceStore.EXTRA_INTENT_CARRIER_ERROR); + finishAndRemoveTask(); + return; + } + if (mSubId != SubscriptionManager.getDefaultSubscriptionId()) { + loge("Unable to start SliceStore on the non-default data subscription: " + mSubId); + SliceStoreBroadcastReceiver.sendSliceStoreResponse( + intent, SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA); + finishAndRemoveTask(); + return; + } + + SliceStoreBroadcastReceiver.updateSliceStoreActivity(mCapability, this); + + mWebView = new WebView(getApplicationContext()); + setContentView(mWebView); + mWebView.loadUrl(mUrl.toString()); + // TODO(b/245882601): Get back response from WebView + } + + @Override + protected void onDestroy() { + logd("onDestroy: User canceled the purchase by closing the application."); + SliceStoreBroadcastReceiver.sendSliceStoreResponse( + getIntent(), SliceStore.EXTRA_INTENT_CANCELED); + SliceStoreBroadcastReceiver.removeSliceStoreActivity(mCapability); + super.onDestroy(); + } + + private @Nullable URL getUrl() { + String url = getApplicationContext().getSystemService(CarrierConfigManager.class) + .getConfigForSubId(mSubId).getString( + CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING); + try { + return new URL(url); + } catch (MalformedURLException e) { + loge("Invalid URL: " + url); + } + return null; + } + + private static void logd(@NonNull String s) { + Log.d(TAG, s); + } + + private static void loge(@NonNull String s) { + Log.e(TAG, s); + } +} diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java new file mode 100644 index 000000000000..7eb851dcdd58 --- /dev/null +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.carrierdefaultapp; + +import android.annotation.NonNull; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.os.UserHandle; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import android.webkit.WebView; + +import com.android.phone.slicestore.SliceStore; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; + +/** + * The SliceStoreBroadcastReceiver listens for {@link SliceStore#ACTION_START_SLICE_STORE} from the + * SliceStore in the phone process to start the SliceStore application. It displays the network + * boost notification to the user and will start the {@link SliceStoreActivity} to display the + * {@link WebView} to purchase network boosts from the user's carrier. + */ +public class SliceStoreBroadcastReceiver extends BroadcastReceiver{ + private static final String TAG = "SliceStoreBroadcastReceiver"; + + /** Weak references to {@link SliceStoreActivity} for each capability, if it exists. */ + private static final Map<Integer, WeakReference<SliceStoreActivity>> sSliceStoreActivities = + new HashMap<>(); + + /** Channel ID for the network boost notification. */ + private static final String NETWORK_BOOST_NOTIFICATION_CHANNEL_ID = "network_boost"; + /** Tag for the network boost notification. */ + public static final String NETWORK_BOOST_NOTIFICATION_TAG = "SliceStore.Notification"; + /** Action for when the user clicks the "Not now" button on the network boost notification. */ + private static final String ACTION_NOTIFICATION_CANCELED = + "com.android.phone.slicestore.action.NOTIFICATION_CANCELED"; + + /** + * Create a weak reference to {@link SliceStoreActivity}. The reference will be removed when + * {@link SliceStoreActivity#onDestroy()} is called. + * + * @param capability The premium capability requested. + * @param sliceStoreActivity The instance of SliceStoreActivity. + */ + public static void updateSliceStoreActivity(@TelephonyManager.PremiumCapability int capability, + @NonNull SliceStoreActivity sliceStoreActivity) { + sSliceStoreActivities.put(capability, new WeakReference<>(sliceStoreActivity)); + } + + /** + * Remove the weak reference to {@link SliceStoreActivity} when + * {@link SliceStoreActivity#onDestroy()} is called. + * + * @param capability The premium capability requested. + */ + public static void removeSliceStoreActivity( + @TelephonyManager.PremiumCapability int capability) { + sSliceStoreActivities.remove(capability); + } + + /** + * Send the PendingIntent containing the corresponding SliceStore response. + * + * @param intent The Intent containing the PendingIntent extra. + * @param extra The extra to get the PendingIntent to send. + */ + public static void sendSliceStoreResponse(@NonNull Intent intent, @NonNull String extra) { + PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class); + if (pendingIntent == null) { + loge("PendingIntent does not exist for extra: " + extra); + return; + } + try { + pendingIntent.send(); + } catch (PendingIntent.CanceledException e) { + loge("Unable to send " + getPendingIntentType(extra) + " intent: " + e); + } + } + + /** + * Check whether the Intent is valid and can be used to complete purchases in the SliceStore. + * This checks that all necessary extras exist and that the values are valid. + * + * @param intent The intent to check + * @return {@code true} if the intent is valid and {@code false} otherwise. + */ + public static boolean isIntentValid(@NonNull Intent intent) { + int phoneId = intent.getIntExtra(SliceStore.EXTRA_PHONE_ID, + SubscriptionManager.INVALID_PHONE_INDEX); + if (phoneId == SubscriptionManager.INVALID_PHONE_INDEX) { + loge("isIntentValid: invalid phone index: " + phoneId); + return false; + } + + int subId = intent.getIntExtra(SliceStore.EXTRA_SUB_ID, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + loge("isIntentValid: invalid subscription ID: " + subId); + return false; + } + + int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY, + SliceStore.PREMIUM_CAPABILITY_INVALID); + if (capability == SliceStore.PREMIUM_CAPABILITY_INVALID) { + loge("isIntentValid: invalid premium capability: " + capability); + return false; + } + + String appName = intent.getStringExtra(SliceStore.EXTRA_REQUESTING_APP_NAME); + if (TextUtils.isEmpty(appName)) { + loge("isIntentValid: empty requesting application name: " + appName); + return false; + } + + return isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_CANCELED) + && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_CARRIER_ERROR) + && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_REQUEST_FAILED) + && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA); + } + + private static boolean isPendingIntentValid(@NonNull Intent intent, @NonNull String extra) { + String intentType = getPendingIntentType(extra); + PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class); + if (pendingIntent == null) { + loge("isPendingIntentValid: " + intentType + " intent not found."); + return false; + } else if (pendingIntent.getCreatorPackage().equals(TelephonyManager.PHONE_PROCESS_NAME)) { + return true; + } + loge("isPendingIntentValid: " + intentType + " intent was created by " + + pendingIntent.getCreatorPackage() + " instead of the phone process."); + return false; + } + + @NonNull private static String getPendingIntentType(@NonNull String extra) { + switch (extra) { + case SliceStore.EXTRA_INTENT_CANCELED: return "canceled"; + case SliceStore.EXTRA_INTENT_CARRIER_ERROR: return "carrier error"; + case SliceStore.EXTRA_INTENT_REQUEST_FAILED: return "request failed"; + case SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA: return "not default data"; + default: { + loge("Unknown pending intent extra: " + extra); + return "unknown(" + extra + ")"; + } + } + } + + @Override + public void onReceive(@NonNull Context context, @NonNull Intent intent) { + logd("onReceive intent: " + intent.getAction()); + switch (intent.getAction()) { + case SliceStore.ACTION_START_SLICE_STORE: + onDisplayBoosterNotification(context, intent); + break; + case SliceStore.ACTION_SLICE_STORE_RESPONSE_TIMEOUT: + onTimeout(context, intent); + break; + case ACTION_NOTIFICATION_CANCELED: + onUserCanceled(context, intent); + break; + default: + loge("Received unknown action: " + intent.getAction()); + } + } + + private void onDisplayBoosterNotification(@NonNull Context context, @NonNull Intent intent) { + if (!isIntentValid(intent)) { + sendSliceStoreResponse(intent, SliceStore.EXTRA_INTENT_REQUEST_FAILED); + return; + } + + context.getSystemService(NotificationManager.class).createNotificationChannel( + new NotificationChannel(NETWORK_BOOST_NOTIFICATION_CHANNEL_ID, + context.getResources().getString( + R.string.network_boost_notification_channel), + NotificationManager.IMPORTANCE_DEFAULT)); + + Notification notification = + new Notification.Builder(context, NETWORK_BOOST_NOTIFICATION_CHANNEL_ID) + .setContentTitle(String.format(context.getResources().getString( + R.string.network_boost_notification_title), + intent.getStringExtra(SliceStore.EXTRA_REQUESTING_APP_NAME))) + .setContentText(context.getResources().getString( + R.string.network_boost_notification_detail)) + .setSmallIcon(R.drawable.ic_network_boost) + .setContentIntent(createContentIntent(context, intent, 1)) + .setDeleteIntent(intent.getParcelableExtra( + SliceStore.EXTRA_INTENT_CANCELED, PendingIntent.class)) + // Add an action for the "Not now" button, which has the same behavior as + // the user canceling or closing the notification. + .addAction(new Notification.Action.Builder( + Icon.createWithResource(context, R.drawable.ic_network_boost), + context.getResources().getString( + R.string.network_boost_notification_button_not_now), + createCanceledIntent(context, intent)).build()) + // Add an action for the "Manage" button, which has the same behavior as + // the user clicking on the notification. + .addAction(new Notification.Action.Builder( + Icon.createWithResource(context, R.drawable.ic_network_boost), + context.getResources().getString( + R.string.network_boost_notification_button_manage), + createContentIntent(context, intent, 2)).build()) + .build(); + + int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY, + SliceStore.PREMIUM_CAPABILITY_INVALID); + logd("Display the booster notification for capability " + + TelephonyManager.convertPremiumCapabilityToString(capability)); + context.getSystemService(NotificationManager.class).notifyAsUser( + NETWORK_BOOST_NOTIFICATION_TAG, capability, notification, UserHandle.ALL); + } + + /** + * Create the intent for when the user clicks on the "Manage" button on the network boost + * notification or the notification itself. This will open {@link SliceStoreActivity}. + * + * @param context The Context to create the intent for. + * @param intent The source Intent used to launch the SliceStore application. + * @param requestCode The request code for the PendingIntent. + * + * @return The intent to start {@link SliceStoreActivity}. + */ + @NonNull private PendingIntent createContentIntent(@NonNull Context context, + @NonNull Intent intent, int requestCode) { + Intent i = new Intent(context, SliceStoreActivity.class); + i.setComponent(ComponentName.unflattenFromString( + "com.android.carrierdefaultapp/.SliceStoreActivity")); + i.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT + | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + i.putExtras(intent); + return PendingIntent.getActivityAsUser(context, requestCode, i, + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, null /* options */, + UserHandle.CURRENT); + } + + /** + * Create the canceled intent for when the user clicks the "Not now" button on the network boost + * notification. This will send {@link #ACTION_NOTIFICATION_CANCELED} and has the same function + * as if the user had canceled or removed the notification. + * + * @param context The Context to create the intent for. + * @param intent The source Intent used to launch the SliceStore application. + * + * @return The canceled intent. + */ + @NonNull private PendingIntent createCanceledIntent(@NonNull Context context, + @NonNull Intent intent) { + Intent i = new Intent(ACTION_NOTIFICATION_CANCELED); + i.setComponent(ComponentName.unflattenFromString( + "com.android.carrierdefaultapp/.SliceStoreBroadcastReceiver")); + i.putExtras(intent); + return PendingIntent.getBroadcast(context, 0, i, + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE); + } + + private void onTimeout(@NonNull Context context, @NonNull Intent intent) { + int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY, + SliceStore.PREMIUM_CAPABILITY_INVALID); + logd("Purchase capability " + TelephonyManager.convertPremiumCapabilityToString(capability) + + " timed out."); + if (sSliceStoreActivities.get(capability) == null) { + // Notification is still active + logd("Closing booster notification since the user did not respond in time."); + context.getSystemService(NotificationManager.class).cancelAsUser( + NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL); + } else { + // Notification was dismissed but SliceStoreActivity is still active + logd("Closing SliceStore WebView since the user did not complete the purchase " + + "in time."); + sSliceStoreActivities.get(capability).get().finishAndRemoveTask(); + // TODO: Display a toast to indicate timeout for better UX? + } + } + + private void onUserCanceled(@NonNull Context context, @NonNull Intent intent) { + int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY, + SliceStore.PREMIUM_CAPABILITY_INVALID); + logd("onUserCanceled: " + TelephonyManager.convertPremiumCapabilityToString(capability)); + context.getSystemService(NotificationManager.class) + .cancelAsUser(NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL); + sendSliceStoreResponse(intent, SliceStore.EXTRA_INTENT_CANCELED); + } + + private static void logd(String s) { + Log.d(TAG, s); + } + + private static void loge(String s) { + Log.e(TAG, s); + } +} diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index 2c24bf14117a..92ce77282cb9 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -1,4 +1,5 @@ -<resources> +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="app_name">CredentialManager</string> <string name="string_cancel">Cancel</string> <string name="string_continue">Continue</string> @@ -12,4 +13,7 @@ <string name="choose_create_option_title">Create a passkey at</string> <string name="choose_sign_in_title">Use saved sign in</string> <string name="create_passkey_at">Create passkey at</string> + <string name="use_provider_for_all_title">Use <xliff:g id="providerInfoName">%1$s</xliff:g> for all your sign-ins?</string> + <string name="set_as_default">Set as default</string> + <string name="use_once">Use once</string> </resources>
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 489cc2791370..ec0c5b708abe 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -21,6 +21,7 @@ import android.app.slice.Slice import android.app.slice.SliceSpec import android.content.Context import android.content.Intent +import android.credentials.ui.Constants import android.credentials.ui.Entry import android.credentials.ui.ProviderData import android.credentials.ui.RequestInfo @@ -60,7 +61,7 @@ class CredentialManagerRepo( ) ?: testProviderList() resultReceiver = intent.getParcelableExtra( - RequestInfo.EXTRA_RESULT_RECEIVER, + Constants.EXTRA_RESULT_RECEIVER, ResultReceiver::class.java ) } @@ -118,46 +119,42 @@ class CredentialManagerRepo( // TODO: below are prototype functionalities. To be removed for productionization. private fun testProviderList(): List<ProviderData> { return listOf( - ProviderData( + ProviderData.Builder( "com.google", - listOf<Entry>( - newEntry(1, "elisa.beckett@gmail.com", "Elisa Backett", - "20 passwords and 7 passkeys saved"), - newEntry(2, "elisa.work@google.com", "Elisa Backett Work", - "20 passwords and 7 passkeys saved"), - ), - listOf<Entry>( - newEntry(3, "Go to Settings", "", - "20 passwords and 7 passkeys saved"), - newEntry(4, "Switch Account", "", - "20 passwords and 7 passkeys saved"), - ), - null - ), - ProviderData( + "Google Password Manager", + Icon.createWithResource(context, R.drawable.ic_launcher_foreground)) + .setCredentialEntries( + listOf<Entry>( + newEntry(1, "elisa.beckett@gmail.com", "Elisa Backett", + "20 passwords and 7 passkeys saved"), + newEntry(2, "elisa.work@google.com", "Elisa Backett Work", + "20 passwords and 7 passkeys saved"), + ) + ).setActionChips( + listOf<Entry>( + newEntry(3, "Go to Settings", "", + "20 passwords and 7 passkeys saved"), + newEntry(4, "Switch Account", "", + "20 passwords and 7 passkeys saved"), + ), + ).build(), + ProviderData.Builder( "com.dashlane", - listOf<Entry>( - newEntry(5, "elisa.beckett@dashlane.com", "Elisa Backett", - "20 passwords and 7 passkeys saved"), - newEntry(6, "elisa.work@dashlane.com", "Elisa Backett Work", - "20 passwords and 7 passkeys saved"), - ), - listOf<Entry>( - newEntry(7, "Manage Accounts", "Manage your accounts in the dashlane app", - "20 passwords and 7 passkeys saved"), - ), - null - ), - ProviderData( - "com.lastpass", - listOf<Entry>( - newEntry(8, "elisa.beckett@lastpass.com", "Elisa Backett", - "20 passwords and 7 passkeys saved"), - ), - listOf<Entry>(), - null - ) - + "Dashlane", + Icon.createWithResource(context, R.drawable.ic_launcher_foreground)) + .setCredentialEntries( + listOf<Entry>( + newEntry(1, "elisa.beckett@dashlane.com", "Elisa Backett", + "20 passwords and 7 passkeys saved"), + newEntry(2, "elisa.work@dashlane.com", "Elisa Backett Work", + "20 passwords and 7 passkeys saved"), + ) + ).setActionChips( + listOf<Entry>( + newEntry(3, "Manage Accounts", "Manage your accounts in the dashlane app", + "20 passwords and 7 passkeys saved"), + ), + ).build(), ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt index f4d60b5f77cc..82fce9f7a98d 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt @@ -83,6 +83,8 @@ fun CreatePasskeyScreen( onOptionSelected = {viewModel.onMoreOptionsRowSelected(it)} ) CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard( + providerInfo = uiState.selectedProvider!!, + onDefaultOrNotSelected = {viewModel.onDefaultOrNotSelected(it)} ) } }, @@ -282,10 +284,37 @@ fun MoreOptionsSelectionCard( @ExperimentalMaterialApi @Composable fun MoreOptionsRowIntroCard( + providerInfo: ProviderInfo, + onDefaultOrNotSelected: (String) -> Unit, ) { Card( backgroundColor = lightBackgroundColor, ) { + Column() { + Text( + text = stringResource(R.string.use_provider_for_all_title, providerInfo.name), + style = Typography.subtitle1, + modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) + ) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp) + ) { + CancelButton( + stringResource(R.string.use_once), + onclick = { onDefaultOrNotSelected(providerInfo.name) } + ) + ConfirmButton( + stringResource(R.string.set_as_default), + onclick = { onDefaultOrNotSelected(providerInfo.name) } + ) + } + Divider( + thickness = 18.dp, + color = Color.Transparent, + modifier = Modifier.padding(bottom = 40.dp) + ) + } } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt index 3cf81da1fbaa..ff44e2ee1b06 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt @@ -112,4 +112,12 @@ class CreatePasskeyViewModel( CredentialManagerRepo.getInstance().onCancel() dialogResult.value = DialogResult(ResultState.CANCELED) } + + fun onDefaultOrNotSelected(providerName: String) { + uiState = uiState.copy( + currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, + selectedProvider = getProviderInfoByName(providerName) + ) + // TODO: implement the if choose as default or not logic later + } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/CredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/CredentialEntryUi.kt new file mode 100644 index 000000000000..d6f1b5f5c8e9 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/CredentialEntryUi.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack + +import android.app.slice.Slice +import android.graphics.drawable.Icon + +/** + * UI representation for a credential entry used during the get credential flow. + * + * TODO: move to jetpack. + */ +abstract class CredentialEntryUi( + val credentialTypeIcon: Icon, + val profileIcon: Icon?, + val lastUsedTimeMillis: Long?, + val note: CharSequence?, +) { + companion object { + fun fromSlice(slice: Slice): CredentialEntryUi { + return when (slice.spec?.type) { + TYPE_PUBLIC_KEY_CREDENTIAL -> PasskeyCredentialEntryUi.fromSlice(slice) + TYPE_PASSWORD_CREDENTIAL -> PasswordCredentialEntryUi.fromSlice(slice) + else -> throw IllegalArgumentException("Unexpected type: ${slice.spec?.type}") + } + } + + const val TYPE_PUBLIC_KEY_CREDENTIAL: String = + "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" + const val TYPE_PASSWORD_CREDENTIAL: String = "androidx.credentials.TYPE_PASSWORD" + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasskeyCredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasskeyCredentialEntryUi.kt new file mode 100644 index 000000000000..bb3b206500b4 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasskeyCredentialEntryUi.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack + +import android.app.slice.Slice +import android.credentials.ui.Entry +import android.graphics.drawable.Icon + +class PasskeyCredentialEntryUi( + val userName: CharSequence, + val userDisplayName: CharSequence?, + credentialTypeIcon: Icon, + profileIcon: Icon?, + lastUsedTimeMillis: Long?, + note: CharSequence?, +) : CredentialEntryUi(credentialTypeIcon, profileIcon, lastUsedTimeMillis, note) { + companion object { + fun fromSlice(slice: Slice): CredentialEntryUi { + var userName: CharSequence? = null + var userDisplayName: CharSequence? = null + var credentialTypeIcon: Icon? = null + var profileIcon: Icon? = null + var lastUsedTimeMillis: Long? = null + var note: CharSequence? = null + + val items = slice.items + items.forEach { + if (it.hasHint(Entry.HINT_USER_NAME)) { + userName = it.text + } else if (it.hasHint(Entry.HINT_PASSKEY_USER_DISPLAY_NAME)) { + userDisplayName = it.text + } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) { + credentialTypeIcon = it.icon + } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) { + profileIcon = it.icon + } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) { + lastUsedTimeMillis = it.long + } else if (it.hasHint(Entry.HINT_NOTE)) { + note = it.text + } + } + // TODO: fail NPE more elegantly. + return PasskeyCredentialEntryUi( + userName!!, userDisplayName, credentialTypeIcon!!, + profileIcon, lastUsedTimeMillis, note, + ) + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasswordCredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasswordCredentialEntryUi.kt new file mode 100644 index 000000000000..7311b7081343 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasswordCredentialEntryUi.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack + +import android.app.slice.Slice +import android.credentials.ui.Entry +import android.graphics.drawable.Icon + +/** + * UI representation for a password credential entry used during the get credential flow. + * + * TODO: move to jetpack. + */ +class PasswordCredentialEntryUi( + val userName: CharSequence, + val password: CharSequence, + credentialTypeIcon: Icon, + profileIcon: Icon?, + lastUsedTimeMillis: Long?, + note: CharSequence?, +) : CredentialEntryUi(credentialTypeIcon, profileIcon, lastUsedTimeMillis, note) { + companion object { + fun fromSlice(slice: Slice): CredentialEntryUi { + var userName: CharSequence? = null + var password: CharSequence? = null + var credentialTypeIcon: Icon? = null + var profileIcon: Icon? = null + var lastUsedTimeMillis: Long? = null + var note: CharSequence? = null + + val items = slice.items + items.forEach { + if (it.hasHint(Entry.HINT_USER_NAME)) { + userName = it.text + } else if (it.hasHint(Entry.HINT_PASSWORD_VALUE)) { + password = it.text + } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) { + credentialTypeIcon = it.icon + } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) { + profileIcon = it.icon + } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) { + lastUsedTimeMillis = it.long + } else if (it.hasHint(Entry.HINT_NOTE)) { + note = it.text + } + } + // TODO: fail NPE more elegantly. + return PasswordCredentialEntryUi( + userName!!, password!!, credentialTypeIcon!!, + profileIcon, lastUsedTimeMillis, note, + ) + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/SaveEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/SaveEntryUi.kt new file mode 100644 index 000000000000..fad3309fb86f --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/SaveEntryUi.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack + +import android.app.slice.Slice +import android.credentials.ui.Entry +import android.graphics.drawable.Icon + +/** + * UI representation for a save entry used during the create credential flow. + * + * TODO: move to jetpack. + */ +class SaveEntryUi( + val userProviderAccountName: CharSequence, + val credentialTypeIcon: Icon?, + val profileIcon: Icon?, + val passwordCount: Int?, + val passkeyCount: Int?, + val totalCredentialCount: Int?, + val lastUsedTimeMillis: Long?, +) { + companion object { + fun fromSlice(slice: Slice): SaveEntryUi { + var userProviderAccountName: CharSequence? = null + var credentialTypeIcon: Icon? = null + var profileIcon: Icon? = null + var passwordCount: Int? = null + var passkeyCount: Int? = null + var totalCredentialCount: Int? = null + var lastUsedTimeMillis: Long? = null + + + val items = slice.items + items.forEach { + if (it.hasHint(Entry.HINT_USER_PROVIDER_ACCOUNT_NAME)) { + userProviderAccountName = it.text + } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) { + credentialTypeIcon = it.icon + } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) { + profileIcon = it.icon + } else if (it.hasHint(Entry.HINT_PASSWORD_COUNT)) { + passwordCount = it.int + } else if (it.hasHint(Entry.HINT_PASSKEY_COUNT)) { + passkeyCount = it.int + } else if (it.hasHint(Entry.HINT_TOTAL_CREDENTIAL_COUNT)) { + totalCredentialCount = it.int + } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) { + lastUsedTimeMillis = it.long + } + } + // TODO: fail NPE more elegantly. + return SaveEntryUi( + userProviderAccountName!!, credentialTypeIcon, profileIcon, + passwordCount, passkeyCount, totalCredentialCount, lastUsedTimeMillis, + ) + } + } +} diff --git a/packages/SettingsLib/ActivityEmbedding/Android.bp b/packages/SettingsLib/ActivityEmbedding/Android.bp index 332bebffb637..c35fb3b17880 100644 --- a/packages/SettingsLib/ActivityEmbedding/Android.bp +++ b/packages/SettingsLib/ActivityEmbedding/Android.bp @@ -26,4 +26,9 @@ android_library { "androidx.window.extensions", "androidx.window.sidecar", ], + + apex_available: [ + "//apex_available:platform", + "com.android.permission", + ], } diff --git a/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml b/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml index 27425589c822..0949e1defc2f 100644 --- a/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml +++ b/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml @@ -21,6 +21,7 @@ <uses-sdk android:minSdkVersion="21" /> <application> + <uses-library android:name="org.apache.http.legacy" android:required="false" /> <uses-library android:name="androidx.window.extensions" android:required="false" /> <uses-library android:name="androidx.window.sidecar" android:required="false" /> </application> diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle index 811cdd804f8b..68c63dad6726 100644 --- a/packages/SettingsLib/Spa/build.gradle +++ b/packages/SettingsLib/Spa/build.gradle @@ -17,6 +17,7 @@ buildscript { ext { spa_min_sdk = 21 + spa_target_sdk = 33 jetpack_compose_version = '1.2.0-alpha04' jetpack_compose_compiler_version = '1.3.2' jetpack_compose_material3_version = '1.0.0-alpha06' diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml index 0a4972fac1bb..f1a24aff4319 100644 --- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml +++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml @@ -17,6 +17,8 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.settingslib.spa.gallery"> + <uses-sdk android:minSdkVersion="21"/> + <application android:name=".GalleryApplication" android:icon="@mipmap/ic_launcher" @@ -32,14 +34,24 @@ </intent-filter> </activity> + <provider + android:name=".GalleryEntryProvider" + android:authorities="com.android.spa.gallery.provider" + android:enabled="true" + android:exported="false"> + </provider> + <activity - android:name=".GalleryDebugActivity" + android:name="com.android.settingslib.spa.framework.debug.BlankActivity" + android:exported="true"> + </activity> + <activity + android:name="com.android.settingslib.spa.framework.debug.DebugActivity" android:exported="true"> </activity> - <provider - android:name=".GalleryEntryProvider" - android:authorities="com.android.spa.gallery.provider" + android:name="com.android.settingslib.spa.framework.debug.DebugProvider" + android:authorities="com.android.spa.gallery.debug" android:enabled="true" android:exported="false"> </provider> diff --git a/packages/SettingsLib/Spa/gallery/build.gradle b/packages/SettingsLib/Spa/gallery/build.gradle index 551a0b107e3a..c1ce7d96702d 100644 --- a/packages/SettingsLib/Spa/gallery/build.gradle +++ b/packages/SettingsLib/Spa/gallery/build.gradle @@ -21,12 +21,12 @@ plugins { android { namespace 'com.android.settingslib.spa.gallery' - compileSdk 33 + compileSdk spa_target_sdk defaultConfig { applicationId "com.android.settingslib.spa.gallery" minSdk spa_min_sdk - targetSdk 33 + targetSdk spa_target_sdk versionCode 1 versionName "1.0" } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index acb22dac9854..4af25893ea37 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -25,6 +25,7 @@ import com.android.settingslib.spa.gallery.home.HomePageProvider import com.android.settingslib.spa.gallery.page.ArgumentPageProvider import com.android.settingslib.spa.gallery.page.FooterPageProvider import com.android.settingslib.spa.gallery.page.IllustrationPageProvider +import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider import com.android.settingslib.spa.gallery.page.SliderPageProvider import com.android.settingslib.spa.gallery.preference.MainSwitchPreferencePageProvider @@ -66,6 +67,7 @@ object GallerySpaEnvironment : SpaEnvironment() { IllustrationPageProvider, CategoryPageProvider, ActionButtonPageProvider, + ProgressBarPageProvider, ), rootPages = listOf( HomePageProvider.createSettingsPage(), diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt index e40775a95813..7fd49db93748 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt @@ -31,6 +31,7 @@ import com.android.settingslib.spa.gallery.page.ArgumentPageModel import com.android.settingslib.spa.gallery.page.ArgumentPageProvider import com.android.settingslib.spa.gallery.page.FooterPageProvider import com.android.settingslib.spa.gallery.page.IllustrationPageProvider +import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider import com.android.settingslib.spa.gallery.page.SliderPageProvider import com.android.settingslib.spa.gallery.preference.PreferenceMainPageProvider @@ -54,6 +55,7 @@ object HomePageProvider : SettingsPageProvider { IllustrationPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + ProgressBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ) } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt new file mode 100644 index 000000000000..dc45df4a0374 --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.gallery.page + +import android.os.Bundle +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.SystemUpdate +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import com.android.settingslib.spa.framework.common.SettingsPage +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.preference.ProgressBarPreference +import com.android.settingslib.spa.widget.preference.ProgressBarPreferenceModel +import com.android.settingslib.spa.widget.preference.ProgressBarWithDataPreference +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.CircularLoadingBar +import com.android.settingslib.spa.widget.ui.CircularProgressBar +import com.android.settingslib.spa.widget.ui.LinearLoadingBar +import kotlinx.coroutines.delay + +private const val TITLE = "Sample ProgressBar" + +object ProgressBarPageProvider : SettingsPageProvider { + override val name = "ProgressBar" + + fun buildInjectEntry(): SettingsEntryBuilder { + return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name)) + .setIsAllowSearch(true) + .setUiLayoutFn { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } + } + + @Composable + override fun Page(arguments: Bundle?) { + // Mocks a loading time of 2 seconds. + var loading by remember { mutableStateOf(true) } + LaunchedEffect(Unit) { + delay(2000) + loading = false + } + + RegularScaffold(title = TITLE) { + // Auto update the progress and finally jump tp 0.4f. + var progress by remember { mutableStateOf(0f) } + LaunchedEffect(Unit) { + delay(2000) + while (progress < 1f) { + delay(100) + progress += 0.01f + } + delay(500) + progress = 0.4f + } + + // Show as a placeholder for progress bar + LargeProgressBar(progress) + // The remaining information only shows after loading complete. + if (!loading) { + SimpleProgressBar() + ProgressBarWithData() + CircularProgressBar(progress = progress, radius = 160f) + } + } + + // Add loading bar examples, running for 2 seconds. + LinearLoadingBar(isLoading = loading, yOffset = 64.dp) + CircularLoadingBar(isLoading = loading) + } +} + +@Composable +private fun LargeProgressBar(progress: Float) { + ProgressBarPreference(object : ProgressBarPreferenceModel { + override val title = "Large Progress Bar" + override val progress = progress + override val height = 20f + }) +} + +@Composable +private fun SimpleProgressBar() { + ProgressBarPreference(object : ProgressBarPreferenceModel { + override val title = "Simple Progress Bar" + override val progress = 0.2f + override val icon = Icons.Outlined.SystemUpdate + }) +} + +@Composable +private fun ProgressBarWithData() { + ProgressBarWithDataPreference(model = object : ProgressBarPreferenceModel { + override val title = "Progress Bar with Data" + override val progress = 0.2f + override val icon = Icons.Outlined.Delete + }, data = "25G") +} + +@Preview(showBackground = true) +@Composable +private fun ProgressBarPagePreview() { + SettingsTheme { + ProgressBarPageProvider.Page(null) + } +} diff --git a/packages/SettingsLib/Spa/spa/AndroidManifest.xml b/packages/SettingsLib/Spa/spa/AndroidManifest.xml index 410bcdb36782..62800bd34217 100644 --- a/packages/SettingsLib/Spa/spa/AndroidManifest.xml +++ b/packages/SettingsLib/Spa/spa/AndroidManifest.xml @@ -14,4 +14,7 @@ limitations under the License. --> -<manifest package="com.android.settingslib.spa" /> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.settingslib.spa"> + <uses-sdk android:minSdkVersion="21"/> +</manifest> diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle index 7e05e75804a7..c5874113ef32 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle +++ b/packages/SettingsLib/Spa/spa/build.gradle @@ -21,11 +21,11 @@ plugins { android { namespace 'com.android.settingslib.spa' - compileSdk 33 + compileSdk spa_target_sdk defaultConfig { minSdk spa_min_sdk - targetSdk 33 + targetSdk spa_target_sdk } sourceSets { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt index 532f63b67c5d..d6317085e4f9 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt @@ -16,21 +16,22 @@ package com.android.settingslib.spa.framework -import android.content.ComponentName import android.content.ContentProvider import android.content.ContentValues import android.content.Context import android.content.Intent -import android.content.Intent.URI_INTENT_SCHEME import android.content.UriMatcher import android.content.pm.ProviderInfo import android.database.Cursor import android.database.MatrixCursor import android.net.Uri import android.util.Log +import com.android.settingslib.spa.framework.common.ColumnEnum +import com.android.settingslib.spa.framework.common.QueryEnum import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsPage import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory +import com.android.settingslib.spa.framework.common.addUri +import com.android.settingslib.spa.framework.common.getColumns private const val TAG = "EntryProvider" @@ -39,117 +40,15 @@ private const val TAG = "EntryProvider" * One can query the provider result by: * $ adb shell content query --uri content://<AuthorityPath>/<QueryPath> * For gallery, AuthorityPath = com.android.spa.gallery.provider - * For SettingsGoogle, AuthorityPath = com.android.settings.spa.provider + * For Settings, AuthorityPath = com.android.settings.spa.provider * Some examples: - * $ adb shell content query --uri content://<AuthorityPath>/page_debug - * $ adb shell content query --uri content://<AuthorityPath>/entry_debug - * $ adb shell content query --uri content://<AuthorityPath>/page_info - * $ adb shell content query --uri content://<AuthorityPath>/entry_info * $ adb shell content query --uri content://<AuthorityPath>/search_sitemap * $ adb shell content query --uri content://<AuthorityPath>/search_static * $ adb shell content query --uri content://<AuthorityPath>/search_dynamic */ open class EntryProvider : ContentProvider() { private val spaEnvironment get() = SpaEnvironmentFactory.instance - - /** - * Enum to define all column names in provider. - */ - enum class ColumnEnum(val id: String) { - // Columns related to page - PAGE_ID("pageId"), - PAGE_NAME("pageName"), - PAGE_ROUTE("pageRoute"), - PAGE_INTENT_URI("pageIntent"), - PAGE_ENTRY_COUNT("entryCount"), - HAS_RUNTIME_PARAM("hasRuntimeParam"), - PAGE_START_ADB("pageStartAdb"), - - // Columns related to entry - ENTRY_ID("entryId"), - ENTRY_NAME("entryName"), - ENTRY_ROUTE("entryRoute"), - ENTRY_INTENT_URI("entryIntent"), - ENTRY_HIERARCHY_PATH("entryPath"), - ENTRY_START_ADB("entryStartAdb"), - - // Columns related to search - ENTRY_TITLE("entryTitle"), - ENTRY_SEARCH_KEYWORD("entrySearchKw"), - } - - /** - * Enum to define all queries supported in the provider. - */ - enum class QueryEnum( - val queryPath: String, - val queryMatchCode: Int, - val columnNames: List<ColumnEnum> - ) { - // For debug - PAGE_DEBUG_QUERY( - "page_debug", 1, - listOf(ColumnEnum.PAGE_START_ADB) - ), - ENTRY_DEBUG_QUERY( - "entry_debug", 2, - listOf(ColumnEnum.ENTRY_START_ADB) - ), - - // page related queries. - PAGE_INFO_QUERY( - "page_info", 100, - listOf( - ColumnEnum.PAGE_ID, - ColumnEnum.PAGE_NAME, - ColumnEnum.PAGE_ROUTE, - ColumnEnum.PAGE_INTENT_URI, - ColumnEnum.PAGE_ENTRY_COUNT, - ColumnEnum.HAS_RUNTIME_PARAM, - ) - ), - - // entry related queries - ENTRY_INFO_QUERY( - "entry_info", 200, - listOf( - ColumnEnum.ENTRY_ID, - ColumnEnum.ENTRY_NAME, - ColumnEnum.ENTRY_ROUTE, - ColumnEnum.ENTRY_INTENT_URI, - ) - ), - - // Search related queries - SEARCH_SITEMAP_QUERY( - "search_sitemap", 300, - listOf( - ColumnEnum.ENTRY_ID, - ColumnEnum.ENTRY_HIERARCHY_PATH, - ) - ), - SEARCH_STATIC_DATA_QUERY( - "search_static", 301, - listOf( - ColumnEnum.ENTRY_ID, - ColumnEnum.ENTRY_TITLE, - ColumnEnum.ENTRY_SEARCH_KEYWORD, - ) - ), - SEARCH_DYNAMIC_DATA_QUERY( - "search_dynamic", 302, - listOf( - ColumnEnum.ENTRY_ID, - ColumnEnum.ENTRY_TITLE, - ColumnEnum.ENTRY_SEARCH_KEYWORD, - ) - ), - } - private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) - private fun addUri(authority: String, query: QueryEnum) { - uriMatcher.addURI(authority, query.queryPath, query.queryMatchCode) - } override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int { TODO("Implement this to handle requests to delete one or more rows") @@ -182,13 +81,9 @@ open class EntryProvider : ContentProvider() { override fun attachInfo(context: Context?, info: ProviderInfo?) { if (info != null) { - addUri(info.authority, QueryEnum.PAGE_DEBUG_QUERY) - addUri(info.authority, QueryEnum.ENTRY_DEBUG_QUERY) - addUri(info.authority, QueryEnum.PAGE_INFO_QUERY) - addUri(info.authority, QueryEnum.ENTRY_INFO_QUERY) - addUri(info.authority, QueryEnum.SEARCH_SITEMAP_QUERY) - addUri(info.authority, QueryEnum.SEARCH_STATIC_DATA_QUERY) - addUri(info.authority, QueryEnum.SEARCH_DYNAMIC_DATA_QUERY) + QueryEnum.SEARCH_SITEMAP_QUERY.addUri(uriMatcher, info.authority) + QueryEnum.SEARCH_STATIC_DATA_QUERY.addUri(uriMatcher, info.authority) + QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.addUri(uriMatcher, info.authority) } super.attachInfo(context, info) } @@ -202,10 +97,6 @@ open class EntryProvider : ContentProvider() { ): Cursor? { return try { when (uriMatcher.match(uri)) { - QueryEnum.PAGE_DEBUG_QUERY.queryMatchCode -> queryPageDebug() - QueryEnum.ENTRY_DEBUG_QUERY.queryMatchCode -> queryEntryDebug() - QueryEnum.PAGE_INFO_QUERY.queryMatchCode -> queryPageInfo() - QueryEnum.ENTRY_INFO_QUERY.queryMatchCode -> queryEntryInfo() QueryEnum.SEARCH_SITEMAP_QUERY.queryMatchCode -> querySearchSitemap() QueryEnum.SEARCH_STATIC_DATA_QUERY.queryMatchCode -> querySearchStaticData() QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.queryMatchCode -> querySearchDynamicData() @@ -219,73 +110,18 @@ open class EntryProvider : ContentProvider() { } } - private fun queryPageDebug(): Cursor { - val entryRepository by spaEnvironment.entryRepository - val cursor = MatrixCursor(QueryEnum.PAGE_DEBUG_QUERY.getColumns()) - for (pageWithEntry in entryRepository.getAllPageWithEntry()) { - val command = createBrowsePageAdbCommand(pageWithEntry.page) - if (command != null) { - cursor.newRow().add(ColumnEnum.PAGE_START_ADB.id, command) - } - } - return cursor - } - - private fun queryEntryDebug(): Cursor { - val entryRepository by spaEnvironment.entryRepository - val cursor = MatrixCursor(QueryEnum.ENTRY_DEBUG_QUERY.getColumns()) - for (entry in entryRepository.getAllEntries()) { - val command = createBrowsePageAdbCommand(entry.containerPage(), entry.id) - if (command != null) { - cursor.newRow().add(ColumnEnum.ENTRY_START_ADB.id, command) - } - } - return cursor - } - - private fun queryPageInfo(): Cursor { - val entryRepository by spaEnvironment.entryRepository - val cursor = MatrixCursor(QueryEnum.PAGE_INFO_QUERY.getColumns()) - for (pageWithEntry in entryRepository.getAllPageWithEntry()) { - val page = pageWithEntry.page - cursor.newRow() - .add(ColumnEnum.PAGE_ID.id, page.id) - .add(ColumnEnum.PAGE_NAME.id, page.displayName) - .add(ColumnEnum.PAGE_ROUTE.id, page.buildRoute()) - .add(ColumnEnum.PAGE_ENTRY_COUNT.id, pageWithEntry.entries.size) - .add(ColumnEnum.HAS_RUNTIME_PARAM.id, if (page.hasRuntimeParam()) 1 else 0) - .add( - ColumnEnum.PAGE_INTENT_URI.id, - createBrowsePageIntent(page).toUri(URI_INTENT_SCHEME) - ) - } - return cursor - } - - private fun queryEntryInfo(): Cursor { - val entryRepository by spaEnvironment.entryRepository - val cursor = MatrixCursor(QueryEnum.ENTRY_INFO_QUERY.getColumns()) - for (entry in entryRepository.getAllEntries()) { - cursor.newRow() - .add(ColumnEnum.ENTRY_ID.id, entry.id) - .add(ColumnEnum.ENTRY_NAME.id, entry.displayName) - .add(ColumnEnum.ENTRY_ROUTE.id, entry.containerPage().buildRoute()) - .add( - ColumnEnum.ENTRY_INTENT_URI.id, - createBrowsePageIntent(entry.containerPage(), entry.id).toUri(URI_INTENT_SCHEME) - ) - } - return cursor - } - private fun querySearchSitemap(): Cursor { val entryRepository by spaEnvironment.entryRepository val cursor = MatrixCursor(QueryEnum.SEARCH_SITEMAP_QUERY.getColumns()) for (entry in entryRepository.getAllEntries()) { if (!entry.isAllowSearch) continue + val intent = entry.containerPage() + .createBrowseIntent(context, spaEnvironment.browseActivityClass, entry.id) + ?: Intent() cursor.newRow() .add(ColumnEnum.ENTRY_ID.id, entry.id) .add(ColumnEnum.ENTRY_HIERARCHY_PATH.id, entryRepository.getEntryPath(entry.id)) + .add(ColumnEnum.ENTRY_INTENT_URI.id, intent.toUri(Intent.URI_INTENT_SCHEME)) } return cursor } @@ -321,54 +157,4 @@ open class EntryProvider : ContentProvider() { searchData?.keyword ?: emptyList<String>() ) } - - private fun createBrowsePageIntent(page: SettingsPage, entryId: String? = null): Intent { - if (!isPageBrowsable(page)) return Intent() - return Intent().setComponent(ComponentName(context!!, spaEnvironment.browseActivityClass!!)) - .apply { - putExtra(BrowseActivity.KEY_DESTINATION, page.buildRoute()) - if (entryId != null) { - putExtra(BrowseActivity.KEY_HIGHLIGHT_ENTRY, entryId) - } - } - } - - private fun createBrowsePageAdbCommand(page: SettingsPage, entryId: String? = null): String? { - if (!isPageBrowsable(page)) return null - val packageName = context!!.packageName - val activityName = spaEnvironment.browseActivityClass!!.name.replace(packageName, "") - val destinationParam = " -e ${BrowseActivity.KEY_DESTINATION} ${page.buildRoute()}" - val highlightParam = - if (entryId != null) " -e ${BrowseActivity.KEY_HIGHLIGHT_ENTRY} $entryId" else "" - return "adb shell am start -n $packageName/$activityName$destinationParam$highlightParam" - } - - private fun isPageBrowsable(page: SettingsPage): Boolean { - return context != null && - spaEnvironment.browseActivityClass != null && - !page.hasRuntimeParam() - } -} - -fun EntryProvider.QueryEnum.getColumns(): Array<String> { - return columnNames.map { it.id }.toTypedArray() -} - -fun EntryProvider.QueryEnum.getIndex(name: EntryProvider.ColumnEnum): Int { - return columnNames.indexOf(name) -} - -fun Cursor.getString(query: EntryProvider.QueryEnum, columnName: EntryProvider.ColumnEnum): String { - return this.getString(query.getIndex(columnName)) -} - -fun Cursor.getInt(query: EntryProvider.QueryEnum, columnName: EntryProvider.ColumnEnum): Int { - return this.getInt(query.getIndex(columnName)) -} - -fun Cursor.getBoolean( - query: EntryProvider.QueryEnum, - columnName: EntryProvider.ColumnEnum -): Boolean { - return this.getInt(query.getIndex(columnName)) == 1 } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt new file mode 100644 index 000000000000..0707429505c8 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.framework.common + +import android.content.UriMatcher + +/** + * Enum to define all column names in provider. + */ +enum class ColumnEnum(val id: String) { + // Columns related to page + PAGE_ID("pageId"), + PAGE_NAME("pageName"), + PAGE_ROUTE("pageRoute"), + PAGE_INTENT_URI("pageIntent"), + PAGE_ENTRY_COUNT("entryCount"), + HAS_RUNTIME_PARAM("hasRuntimeParam"), + PAGE_START_ADB("pageStartAdb"), + + // Columns related to entry + ENTRY_ID("entryId"), + ENTRY_NAME("entryName"), + ENTRY_ROUTE("entryRoute"), + ENTRY_INTENT_URI("entryIntent"), + ENTRY_HIERARCHY_PATH("entryPath"), + ENTRY_START_ADB("entryStartAdb"), + + // Columns related to search + ENTRY_TITLE("entryTitle"), + ENTRY_SEARCH_KEYWORD("entrySearchKw"), +} + +/** + * Enum to define all queries supported in the provider. + */ +enum class QueryEnum( + val queryPath: String, + val queryMatchCode: Int, + val columnNames: List<ColumnEnum> +) { + // For debug + PAGE_DEBUG_QUERY( + "page_debug", 1, + listOf(ColumnEnum.PAGE_START_ADB) + ), + ENTRY_DEBUG_QUERY( + "entry_debug", 2, + listOf(ColumnEnum.ENTRY_START_ADB) + ), + + // page related queries. + PAGE_INFO_QUERY( + "page_info", 100, + listOf( + ColumnEnum.PAGE_ID, + ColumnEnum.PAGE_NAME, + ColumnEnum.PAGE_ROUTE, + ColumnEnum.PAGE_INTENT_URI, + ColumnEnum.PAGE_ENTRY_COUNT, + ColumnEnum.HAS_RUNTIME_PARAM, + ) + ), + + // entry related queries + ENTRY_INFO_QUERY( + "entry_info", 200, + listOf( + ColumnEnum.ENTRY_ID, + ColumnEnum.ENTRY_NAME, + ColumnEnum.ENTRY_ROUTE, + ColumnEnum.ENTRY_INTENT_URI, + ) + ), + + // Search related queries + SEARCH_SITEMAP_QUERY( + "search_sitemap", 300, + listOf( + ColumnEnum.ENTRY_ID, + ColumnEnum.ENTRY_HIERARCHY_PATH, + ColumnEnum.ENTRY_INTENT_URI, + ) + ), + SEARCH_STATIC_DATA_QUERY( + "search_static", 301, + listOf( + ColumnEnum.ENTRY_ID, + ColumnEnum.ENTRY_TITLE, + ColumnEnum.ENTRY_SEARCH_KEYWORD, + ) + ), + SEARCH_DYNAMIC_DATA_QUERY( + "search_dynamic", 302, + listOf( + ColumnEnum.ENTRY_ID, + ColumnEnum.ENTRY_TITLE, + ColumnEnum.ENTRY_SEARCH_KEYWORD, + ) + ), +} + +internal fun QueryEnum.getColumns(): Array<String> { + return columnNames.map { it.id }.toTypedArray() +} + +internal fun QueryEnum.getIndex(name: ColumnEnum): Int { + return columnNames.indexOf(name) +} + +internal fun QueryEnum.addUri(uriMatcher: UriMatcher, authority: String) { + uriMatcher.addURI(authority, queryPath, queryMatchCode) +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt index 8616b9f1ddd3..fb42f01b14f8 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt @@ -37,7 +37,7 @@ interface EntryData { } val LocalEntryDataProvider = - compositionLocalOf<EntryData> { object : EntryData{} } + compositionLocalOf<EntryData> { object : EntryData {} } /** * Defines data of a Settings entry. diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt index 8f63c47b1a9b..07df96e778c4 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt @@ -16,8 +16,13 @@ package com.android.settingslib.spa.framework.common +import android.app.Activity +import android.content.ComponentName +import android.content.Context +import android.content.Intent import android.os.Bundle import androidx.navigation.NamedNavArgument +import com.android.settingslib.spa.framework.BrowseActivity import com.android.settingslib.spa.framework.util.isRuntimeParam import com.android.settingslib.spa.framework.util.navLink import com.android.settingslib.spa.framework.util.normalize @@ -111,6 +116,41 @@ data class SettingsPage( details = formatDisplayTitle() ) } + + fun createBrowseIntent( + context: Context?, + browseActivityClass: Class<out Activity>?, + entryId: String? = null + ): Intent? { + if (!isBrowsable(context, browseActivityClass)) return null + return Intent().setComponent(ComponentName(context!!, browseActivityClass!!)) + .apply { + putExtra(BrowseActivity.KEY_DESTINATION, buildRoute()) + if (entryId != null) { + putExtra(BrowseActivity.KEY_HIGHLIGHT_ENTRY, entryId) + } + } + } + + fun createBrowseAdbCommand( + context: Context?, + browseActivityClass: Class<out Activity>?, + entryId: String? = null + ): String? { + if (!isBrowsable(context, browseActivityClass)) return null + val packageName = context!!.packageName + val activityName = browseActivityClass!!.name.replace(packageName, "") + val destinationParam = " -e ${BrowseActivity.KEY_DESTINATION} ${buildRoute()}" + val highlightParam = + if (entryId != null) " -e ${BrowseActivity.KEY_HIGHLIGHT_ENTRY} $entryId" else "" + return "adb shell am start -n $packageName/$activityName$destinationParam$highlightParam" + } + + fun isBrowsable(context: Context?, browseActivityClass: Class<out Activity>?): Boolean { + return context != null && + browseActivityClass != null && + !hasRuntimeParam() + } } fun String.toHashId(): String { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt index 6f968180e243..301508074f30 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt @@ -14,12 +14,9 @@ * limitations under the License. */ -package com.android.settingslib.spa.framework +package com.android.settingslib.spa.framework.debug -import android.content.Intent -import android.net.Uri import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.Text @@ -33,8 +30,6 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.android.settingslib.spa.R -import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_DESTINATION -import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_HIGHLIGHT_ENTRY import com.android.settingslib.spa.framework.common.LogCategory import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsPage @@ -60,11 +55,10 @@ private const val PARAM_NAME_ENTRY_ID = "eid" /** * The Debug Activity to display all Spa Pages & Entries. * One can open the debug activity by: - * $ adb shell am start -n <Activity> - * For gallery, Activity = com.android.settingslib.spa.gallery/.GalleryDebugActivity - * For SettingsGoogle, Activity = com.android.settings/.spa.SpaDebugActivity + * $ adb shell am start -n <Package>/com.android.settingslib.spa.framework.debug.DebugActivity + * For gallery, Package = com.android.settingslib.spa.gallery */ -open class DebugActivity : ComponentActivity() { +class DebugActivity : ComponentActivity() { private val spaEnvironment get() = SpaEnvironmentFactory.instance override fun onCreate(savedInstanceState: Bundle?) { @@ -79,30 +73,6 @@ open class DebugActivity : ComponentActivity() { } } - private fun displayDebugMessage() { - val entryProviderAuthorities = spaEnvironment.entryProviderAuthorities ?: return - - try { - val query = EntryProvider.QueryEnum.PAGE_INFO_QUERY - contentResolver.query( - Uri.parse("content://$entryProviderAuthorities/${query.queryPath}"), - null, null, null - ).use { cursor -> - while (cursor != null && cursor.moveToNext()) { - val route = cursor.getString(query, EntryProvider.ColumnEnum.PAGE_ROUTE) - val entryCount = cursor.getInt(query, EntryProvider.ColumnEnum.PAGE_ENTRY_COUNT) - val hasRuntimeParam = - cursor.getBoolean(query, EntryProvider.ColumnEnum.HAS_RUNTIME_PARAM) - val message = "Page Info: $route ($entryCount) " + - (if (hasRuntimeParam) "with" else "no") + "-runtime-params" - spaEnvironment.logger.message(TAG, message, category = LogCategory.FRAMEWORK) - } - } - } catch (e: Exception) { - Log.e(TAG, "Provider querying exception:", e) - } - } - @Composable private fun MainContent() { val navController = rememberNavController() @@ -141,11 +111,6 @@ open class DebugActivity : ComponentActivity() { override val title = "List All Entries (${allEntry.size})" override val onClick = navigator(route = ROUTE_All_ENTRIES) }) - Preference(object : PreferenceModel { - override val title = "Query EntryProvider" - override val enabled = isEntryProviderAvailable().toState() - override val onClick = { displayDebugMessage() } - }) } } @@ -177,6 +142,7 @@ open class DebugActivity : ComponentActivity() { @Composable fun OnePage(arguments: Bundle?) { + val context = LocalContext.current val entryRepository by spaEnvironment.entryRepository val id = arguments!!.getString(PARAM_NAME_PAGE_ID, "") val pageWithEntry = entryRepository.getPageWithEntry(id)!! @@ -186,7 +152,9 @@ open class DebugActivity : ComponentActivity() { Text(text = "Entry size: ${pageWithEntry.entries.size}") Preference(model = object : PreferenceModel { override val title = "open page" - override val enabled = isPageClickable(pageWithEntry.page).toState() + override val enabled = + pageWithEntry.page.isBrowsable(context, spaEnvironment.browseActivityClass) + .toState() override val onClick = openPage(pageWithEntry.page) }) EntryList(pageWithEntry.entries) @@ -195,6 +163,7 @@ open class DebugActivity : ComponentActivity() { @Composable fun OneEntry(arguments: Bundle?) { + val context = LocalContext.current val entryRepository by spaEnvironment.entryRepository val id = arguments!!.getString(PARAM_NAME_ENTRY_ID, "") val entry = entryRepository.getEntry(id)!! @@ -202,7 +171,9 @@ open class DebugActivity : ComponentActivity() { RegularScaffold(title = "Entry - ${entry.displayTitle()}") { Preference(model = object : PreferenceModel { override val title = "open entry" - override val enabled = isEntryClickable(entry).toState() + override val enabled = + entry.containerPage().isBrowsable(context, spaEnvironment.browseActivityClass) + .toState() override val onClick = openEntry(entry) }) Text(text = entryContent) @@ -223,12 +194,10 @@ open class DebugActivity : ComponentActivity() { @Composable private fun openPage(page: SettingsPage): (() -> Unit)? { - if (!isPageClickable(page)) return null val context = LocalContext.current + val intent = + page.createBrowseIntent(context, spaEnvironment.browseActivityClass) ?: return null val route = page.buildRoute() - val intent = Intent(context, spaEnvironment.browseActivityClass).apply { - putExtra(KEY_DESTINATION, route) - } return { spaEnvironment.logger.message( TAG, "OpenPage: $route", category = LogCategory.FRAMEWORK @@ -239,13 +208,11 @@ open class DebugActivity : ComponentActivity() { @Composable private fun openEntry(entry: SettingsEntry): (() -> Unit)? { - if (!isEntryClickable(entry)) return null val context = LocalContext.current + val intent = entry.containerPage() + .createBrowseIntent(context, spaEnvironment.browseActivityClass, entry.id) + ?: return null val route = entry.containerPage().buildRoute() - val intent = Intent(context, spaEnvironment.browseActivityClass).apply { - putExtra(KEY_DESTINATION, route) - putExtra(KEY_HIGHLIGHT_ENTRY, entry.id) - } return { spaEnvironment.logger.message( TAG, "OpenEntry: $route", category = LogCategory.FRAMEWORK @@ -253,17 +220,9 @@ open class DebugActivity : ComponentActivity() { context.startActivity(intent) } } - - private fun isEntryProviderAvailable(): Boolean { - return spaEnvironment.entryProviderAuthorities != null - } - - private fun isPageClickable(page: SettingsPage): Boolean { - return spaEnvironment.browseActivityClass != null && !page.hasRuntimeParam() - } - - private fun isEntryClickable(entry: SettingsEntry): Boolean { - return spaEnvironment.browseActivityClass != null && - !entry.containerPage().hasRuntimeParam() - } } + +/** + * A blank activity without any page. + */ +class BlankActivity : ComponentActivity() diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt new file mode 100644 index 000000000000..6c271094de9f --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.framework.debug + +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.content.Intent.URI_INTENT_SCHEME +import android.content.UriMatcher +import android.content.pm.ProviderInfo +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import android.util.Log +import com.android.settingslib.spa.framework.common.ColumnEnum +import com.android.settingslib.spa.framework.common.QueryEnum +import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory +import com.android.settingslib.spa.framework.common.addUri +import com.android.settingslib.spa.framework.common.getColumns + +private const val TAG = "DebugProvider" + +/** + * The content provider to return debug data. + * One can query the provider result by: + * $ adb shell content query --uri content://<AuthorityPath>/<QueryPath> + * For gallery, AuthorityPath = com.android.spa.gallery.debug + * Some examples: + * $ adb shell content query --uri content://<AuthorityPath>/page_debug + * $ adb shell content query --uri content://<AuthorityPath>/entry_debug + * $ adb shell content query --uri content://<AuthorityPath>/page_info + * $ adb shell content query --uri content://<AuthorityPath>/entry_info + */ +class DebugProvider : ContentProvider() { + private val spaEnvironment get() = SpaEnvironmentFactory.instance + private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int { + TODO("Implement this to handle requests to delete one or more rows") + } + + override fun getType(uri: Uri): String? { + TODO( + "Implement this to handle requests for the MIME type of the data" + + "at the given URI" + ) + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + TODO("Implement this to handle requests to insert a new row.") + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array<String>? + ): Int { + TODO("Implement this to handle requests to update one or more rows.") + } + + override fun onCreate(): Boolean { + Log.d(TAG, "onCreate") + return true + } + + override fun attachInfo(context: Context?, info: ProviderInfo?) { + if (info != null) { + QueryEnum.PAGE_DEBUG_QUERY.addUri(uriMatcher, info.authority) + QueryEnum.ENTRY_DEBUG_QUERY.addUri(uriMatcher, info.authority) + QueryEnum.PAGE_INFO_QUERY.addUri(uriMatcher, info.authority) + QueryEnum.ENTRY_INFO_QUERY.addUri(uriMatcher, info.authority) + } + super.attachInfo(context, info) + } + + override fun query( + uri: Uri, + projection: Array<String>?, + selection: String?, + selectionArgs: Array<String>?, + sortOrder: String? + ): Cursor? { + return try { + when (uriMatcher.match(uri)) { + QueryEnum.PAGE_DEBUG_QUERY.queryMatchCode -> queryPageDebug() + QueryEnum.ENTRY_DEBUG_QUERY.queryMatchCode -> queryEntryDebug() + QueryEnum.PAGE_INFO_QUERY.queryMatchCode -> queryPageInfo() + QueryEnum.ENTRY_INFO_QUERY.queryMatchCode -> queryEntryInfo() + else -> throw UnsupportedOperationException("Unknown Uri $uri") + } + } catch (e: UnsupportedOperationException) { + throw e + } catch (e: Exception) { + Log.e(TAG, "Provider querying exception:", e) + null + } + } + + private fun queryPageDebug(): Cursor { + val entryRepository by spaEnvironment.entryRepository + val cursor = MatrixCursor(QueryEnum.PAGE_DEBUG_QUERY.getColumns()) + for (pageWithEntry in entryRepository.getAllPageWithEntry()) { + val command = pageWithEntry.page.createBrowseAdbCommand( + context, + spaEnvironment.browseActivityClass + ) + if (command != null) { + cursor.newRow().add(ColumnEnum.PAGE_START_ADB.id, command) + } + } + return cursor + } + + private fun queryEntryDebug(): Cursor { + val entryRepository by spaEnvironment.entryRepository + val cursor = MatrixCursor(QueryEnum.ENTRY_DEBUG_QUERY.getColumns()) + for (entry in entryRepository.getAllEntries()) { + val command = entry.containerPage() + .createBrowseAdbCommand(context, spaEnvironment.browseActivityClass, entry.id) + if (command != null) { + cursor.newRow().add(ColumnEnum.ENTRY_START_ADB.id, command) + } + } + return cursor + } + + private fun queryPageInfo(): Cursor { + val entryRepository by spaEnvironment.entryRepository + val cursor = MatrixCursor(QueryEnum.PAGE_INFO_QUERY.getColumns()) + for (pageWithEntry in entryRepository.getAllPageWithEntry()) { + val page = pageWithEntry.page + val intent = + page.createBrowseIntent(context, spaEnvironment.browseActivityClass) ?: Intent() + cursor.newRow() + .add(ColumnEnum.PAGE_ID.id, page.id) + .add(ColumnEnum.PAGE_NAME.id, page.displayName) + .add(ColumnEnum.PAGE_ROUTE.id, page.buildRoute()) + .add(ColumnEnum.PAGE_ENTRY_COUNT.id, pageWithEntry.entries.size) + .add(ColumnEnum.HAS_RUNTIME_PARAM.id, if (page.hasRuntimeParam()) 1 else 0) + .add(ColumnEnum.PAGE_INTENT_URI.id, intent.toUri(URI_INTENT_SCHEME)) + } + return cursor + } + + private fun queryEntryInfo(): Cursor { + val entryRepository by spaEnvironment.entryRepository + val cursor = MatrixCursor(QueryEnum.ENTRY_INFO_QUERY.getColumns()) + for (entry in entryRepository.getAllEntries()) { + val intent = entry.containerPage() + .createBrowseIntent(context, spaEnvironment.browseActivityClass, entry.id) + ?: Intent() + cursor.newRow() + .add(ColumnEnum.ENTRY_ID.id, entry.id) + .add(ColumnEnum.ENTRY_NAME.id, entry.displayName) + .add(ColumnEnum.ENTRY_ROUTE.id, entry.containerPage().buildRoute()) + .add(ColumnEnum.ENTRY_INTENT_URI.id, intent.toUri(URI_INTENT_SCHEME)) + } + return cursor + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt index 3fa8c658e198..52c489324699 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt @@ -44,3 +44,6 @@ internal fun materialColorScheme(isDarkTheme: Boolean): ColorScheme { val ColorScheme.divider: Color get() = onSurface.copy(SettingsOpacity.Divider) + +val ColorScheme.surfaceTone: Color + get() = primary.copy(SettingsOpacity.SurfaceTone) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt index 11af6ce0beab..69ddf01b6170 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt @@ -20,4 +20,5 @@ object SettingsOpacity { const val Full = 1f const val Disabled = 0.38f const val Divider = 0.2f + const val SurfaceTone = 0.14f } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt index 6c7432eee16a..8d0a35c371e3 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt @@ -23,7 +23,7 @@ import com.android.settingslib.spa.framework.common.LogEvent import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory @Composable -fun LogEntryEvent(): (event: LogEvent) -> Unit { +fun logEntryEvent(): (event: LogEvent) -> Unit { val entryId = LocalEntryDataProvider.current.entryId ?: return {} return { SpaEnvironmentFactory.instance.logger.event(entryId, it, category = LogCategory.VIEW) @@ -31,9 +31,9 @@ fun LogEntryEvent(): (event: LogEvent) -> Unit { } @Composable -fun WrapOnClickWithLog(onClick: (() -> Unit)?): (() -> Unit)? { +fun wrapOnClickWithLog(onClick: (() -> Unit)?): (() -> Unit)? { if (onClick == null) return null - val logEvent = LogEntryEvent() + val logEvent = logEntryEvent() return { logEvent(LogEvent.ENTRY_CLICK) onClick() @@ -41,9 +41,9 @@ fun WrapOnClickWithLog(onClick: (() -> Unit)?): (() -> Unit)? { } @Composable -fun WrapOnSwitchWithLog(onSwitch: ((checked: Boolean) -> Unit)?): ((checked: Boolean) -> Unit)? { +fun wrapOnSwitchWithLog(onSwitch: ((checked: Boolean) -> Unit)?): ((checked: Boolean) -> Unit)? { if (onSwitch == null) return null - val logEvent = LogEntryEvent() + val logEvent = logEntryEvent() return { val event = if (it) LogEvent.ENTRY_SWITCH_ON else LogEvent.ENTRY_SWITCH_OFF logEvent(event) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt index 9a34dbf36735..6135203ec703 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt @@ -72,7 +72,7 @@ internal fun BaseLayout( } @Composable -private fun BaseIcon( +internal fun BaseIcon( icon: @Composable (() -> Unit)?, modifier: Modifier, paddingStart: Dp, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt index 3e04b16f08cf..db95e23bb52b 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt @@ -28,7 +28,7 @@ import com.android.settingslib.spa.framework.compose.toState import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsShape import com.android.settingslib.spa.framework.theme.SettingsTheme -import com.android.settingslib.spa.framework.util.EntryHighlight +import com.android.settingslib.spa.widget.util.EntryHighlight @Composable fun MainSwitchPreference(model: SwitchPreferenceModel) { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt index 7c0116a582f6..6ebe6bb9dfa3 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt @@ -26,9 +26,9 @@ import com.android.settingslib.spa.framework.common.EntryMacro import com.android.settingslib.spa.framework.common.EntrySearchData import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.compose.stateOf -import com.android.settingslib.spa.framework.util.WrapOnClickWithLog -import com.android.settingslib.spa.framework.util.EntryHighlight +import com.android.settingslib.spa.framework.util.wrapOnClickWithLog import com.android.settingslib.spa.widget.ui.createSettingsIcon +import com.android.settingslib.spa.widget.util.EntryHighlight data class SimplePreferenceMacro( val title: String, @@ -107,7 +107,7 @@ fun Preference( model: PreferenceModel, singleLineSummary: Boolean = false, ) { - val onClickWithLog = WrapOnClickWithLog(model.onClick) + val onClickWithLog = wrapOnClickWithLog(model.onClick) val modifier = remember(model.enabled.value) { if (onClickWithLog != null) { Modifier.clickable( diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt new file mode 100644 index 000000000000..b8c59ad07cfd --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.preference + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.widget.ui.LinearProgressBar +import com.android.settingslib.spa.widget.ui.SettingsTitle + +/** + * The widget model for [ProgressBarPreference] widget. + */ +interface ProgressBarPreferenceModel { + /** + * The title of this [ProgressBarPreference]. + */ + val title: String + + /** + * The progress fraction of the ProgressBar. Should be float in range [0f, 1f] + */ + val progress: Float + + /** + * The icon image for [ProgressBarPreference]. If not specified, hides the icon by default. + */ + val icon: ImageVector? + get() = null + + /** + * The height of the ProgressBar. + */ + val height: Float + get() = 4f + + /** + * Indicates whether to use rounded corner for the progress bars. + */ + val roundedCorner: Boolean + get() = true +} + +/** + * Progress bar preference widget. + * + * Data is provided through [ProgressBarPreferenceModel]. + */ +@Composable +fun ProgressBarPreference(model: ProgressBarPreferenceModel) { + ProgressBarPreference( + title = model.title, + progress = model.progress, + icon = model.icon, + height = model.height, + roundedCorner = model.roundedCorner, + ) +} + +/** + * Progress bar with data preference widget. + */ +@Composable +fun ProgressBarWithDataPreference(model: ProgressBarPreferenceModel, data: String) { + val icon = model.icon + ProgressBarWithDataPreference( + title = model.title, + data = data, + progress = model.progress, + icon = if (icon != null) ({ + Icon(imageVector = icon, contentDescription = null) + }) else null, + height = model.height, + roundedCorner = model.roundedCorner, + ) +} + +@Composable +internal fun ProgressBarPreference( + title: String, + progress: Float, + icon: ImageVector? = null, + height: Float = 4f, + roundedCorner: Boolean = true, +) { + BaseLayout( + title = title, + subTitle = { + LinearProgressBar(progress, height, roundedCorner) + }, + icon = if (icon != null) ({ + Icon(imageVector = icon, contentDescription = null) + }) else null, + ) +} + + +@Composable +internal fun ProgressBarWithDataPreference( + title: String, + data: String, + progress: Float, + icon: (@Composable () -> Unit)? = null, + height: Float = 4f, + roundedCorner: Boolean = true, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(end = SettingsDimension.itemPaddingEnd), + verticalAlignment = Alignment.CenterVertically, + ) { + BaseIcon(icon, Modifier, SettingsDimension.itemPaddingStart) + TitleWithData( + title = title, + data = data, + subTitle = { + LinearProgressBar(progress, height, roundedCorner) + }, + modifier = Modifier + .weight(1f) + .padding(vertical = SettingsDimension.itemPaddingVertical), + ) + } +} + +@Composable +private fun TitleWithData( + title: String, + data: String, + subTitle: @Composable () -> Unit, + modifier: Modifier +) { + Column(modifier) { + Row { + Box(modifier = Modifier.weight(1f)) { + SettingsTitle(title) + } + Text( + text = data, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.titleMedium, + ) + } + subTitle() + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt index 7bca38fdb48f..4ee2af0fa1b7 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.theme.SettingsTheme -import com.android.settingslib.spa.framework.util.EntryHighlight import com.android.settingslib.spa.widget.ui.SettingsSlider +import com.android.settingslib.spa.widget.util.EntryHighlight /** * The widget model for [SliderPreference] widget. diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt index 592a99f4fa5c..2d606193872d 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt @@ -31,9 +31,9 @@ import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.framework.compose.toState import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme -import com.android.settingslib.spa.framework.util.WrapOnSwitchWithLog -import com.android.settingslib.spa.framework.util.EntryHighlight +import com.android.settingslib.spa.framework.util.wrapOnSwitchWithLog import com.android.settingslib.spa.widget.ui.SettingsSwitch +import com.android.settingslib.spa.widget.util.EntryHighlight /** * The widget model for [SwitchPreference] widget. @@ -104,7 +104,7 @@ internal fun InternalSwitchPreference( ) { val checkedValue = checked.value val indication = LocalIndication.current - val onChangeWithLog = WrapOnSwitchWithLog(onCheckedChange) + val onChangeWithLog = wrapOnSwitchWithLog(onCheckedChange) val modifier = remember(checkedValue, changeable.value) { if (checkedValue != null && onChangeWithLog != null) { Modifier.toggleable( diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt index 63de2c821154..fbfcaaa28047 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt @@ -17,7 +17,7 @@ package com.android.settingslib.spa.widget.preference import androidx.compose.runtime.Composable -import com.android.settingslib.spa.framework.util.EntryHighlight +import com.android.settingslib.spa.widget.util.EntryHighlight import com.android.settingslib.spa.widget.ui.SettingsSwitch @Composable diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt new file mode 100644 index 000000000000..1741f134f3d1 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Indeterminate linear progress bar. Expresses an unspecified wait time. + */ +@Composable +fun LinearLoadingBar( + isLoading: Boolean, + xOffset: Dp = 0.dp, + yOffset: Dp = 0.dp +) { + if (isLoading) { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .absoluteOffset(xOffset, yOffset) + ) + } +} + +/** + * Indeterminate circular progress bar. Expresses an unspecified wait time. + */ +@Composable +fun CircularLoadingBar(isLoading: Boolean) { + if (isLoading) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt new file mode 100644 index 000000000000..5d8502db4986 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.ui + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.progressSemantics +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.unit.dp + +/** + * Determinate linear progress bar. Displays the current progress of the whole process. + * + * Rounded corner is supported and enabled by default. + */ +@Composable +fun LinearProgressBar( + progress: Float, + height: Float = 4f, + roundedCorner: Boolean = true +) { + Box(modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)) { + val color = MaterialTheme.colorScheme.onSurface + val trackColor = MaterialTheme.colorScheme.surfaceVariant + Canvas( + Modifier + .progressSemantics(progress) + .fillMaxWidth() + .height(height.dp) + ) { + drawLinearBarTrack(trackColor, roundedCorner) + drawLinearBar(progress, color, roundedCorner) + } + } +} + +private fun DrawScope.drawLinearBar( + endFraction: Float, + color: Color, + roundedCorner: Boolean +) { + val width = endFraction * size.width + drawRoundRect( + color = color, + size = Size(width, size.height), + cornerRadius = if (roundedCorner) CornerRadius( + size.height / 2, + size.height / 2 + ) else CornerRadius.Zero, + ) +} + +private fun DrawScope.drawLinearBarTrack( + color: Color, + roundedCorner: Boolean +) = drawLinearBar(1f, color, roundedCorner) + +/** + * Determinate circular progress bar. Displays the current progress of the whole process. + * + * Displayed in default material3 style, and rounded corner is not supported. + */ +@Composable +fun CircularProgressBar(progress: Float, radius: Float = 40f) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator( + progress = progress, + modifier = Modifier.size(radius.dp, radius.dp) + ) + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt index d8455e46ec4a..48fec3bd8ad7 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt @@ -16,13 +16,16 @@ package com.android.settingslib.spa.widget.ui +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import com.android.settingslib.spa.framework.theme.surfaceTone import kotlin.math.roundToInt @Composable @@ -45,5 +48,8 @@ fun SettingsSlider( valueRange = valueRange.first.toFloat()..valueRange.last.toFloat(), steps = if (showSteps) (valueRange.count() - 2) else 0, onValueChangeFinished = onValueChangeFinished, + colors = SliderDefaults.colors( + inactiveTrackColor = MaterialTheme.colorScheme.surfaceTone + ) ) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt index 82ab0be55002..b9690762845e 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt @@ -20,7 +20,7 @@ import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.State -import com.android.settingslib.spa.framework.util.WrapOnSwitchWithLog +import com.android.settingslib.spa.framework.util.wrapOnSwitchWithLog @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -35,7 +35,7 @@ fun SettingsSwitch( if (checkedValue != null) { Checkbox( checked = checkedValue, - onCheckedChange = WrapOnSwitchWithLog(onCheckedChange), + onCheckedChange = wrapOnSwitchWithLog(onCheckedChange), enabled = changeable.value, ) } else { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryHighlight.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt index 8e24ce042d43..652e54de5e39 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryHighlight.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settingslib.spa.framework.util +package com.android.settingslib.spa.widget.util import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box diff --git a/packages/SettingsLib/Spa/tests/Android.bp b/packages/SettingsLib/Spa/tests/Android.bp index 1ce49fa520b9..74910456471e 100644 --- a/packages/SettingsLib/Spa/tests/Android.bp +++ b/packages/SettingsLib/Spa/tests/Android.bp @@ -34,4 +34,5 @@ android_test { "truth-prebuilt", ], kotlincflags: ["-Xjvm-default=all"], + min_sdk_version: "31", } diff --git a/packages/SettingsLib/Spa/tests/AndroidManifest.xml b/packages/SettingsLib/Spa/tests/AndroidManifest.xml index c224cafa4740..e2db5943ae53 100644 --- a/packages/SettingsLib/Spa/tests/AndroidManifest.xml +++ b/packages/SettingsLib/Spa/tests/AndroidManifest.xml @@ -17,6 +17,8 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.settingslib.spa.tests"> + <uses-sdk android:minSdkVersion="21"/> + <application> <uses-library android:name="android.test.runner" /> </application> diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle index f950e01b9740..b43bf1854c26 100644 --- a/packages/SettingsLib/Spa/tests/build.gradle +++ b/packages/SettingsLib/Spa/tests/build.gradle @@ -21,11 +21,11 @@ plugins { android { namespace 'com.android.settingslib.spa.tests' - compileSdk 33 + compileSdk spa_target_sdk defaultConfig { minSdk spa_min_sdk - targetSdk 33 + targetSdk spa_target_sdk testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt new file mode 100644 index 000000000000..5611f8ce14db --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.preference + +import androidx.compose.ui.semantics.ProgressBarRangeInfo +import androidx.compose.ui.semantics.SemanticsProperties.ProgressBarRangeInfo +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ProgressBarPreferenceTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun title_displayed() { + composeTestRule.setContent { + ProgressBarPreference(object : ProgressBarPreferenceModel { + override val title = "Title" + override val progress = 0.2f + }) + } + composeTestRule.onNodeWithText("Title").assertIsDisplayed() + } + + @Test + fun data_displayed() { + composeTestRule.setContent { + ProgressBarWithDataPreference(model = object : ProgressBarPreferenceModel { + override val title = "Title" + override val progress = 0.2f + }, data = "Data") + } + composeTestRule.onNodeWithText("Title").assertIsDisplayed() + composeTestRule.onNodeWithText("Data").assertIsDisplayed() + } + + @Test + fun progressBar_displayed() { + composeTestRule.setContent { + ProgressBarPreference(object : ProgressBarPreferenceModel { + override val title = "Title" + override val progress = 0.2f + }) + } + + fun progressEqualsTo(progress: Float): SemanticsMatcher = + SemanticsMatcher.expectValue( + ProgressBarRangeInfo, + ProgressBarRangeInfo(progress, 0f..1f, 0) + ) + composeTestRule.onNode(progressEqualsTo(0.2f)).assertIsDisplayed() + } +} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt index 7ae11758a0ce..3e5dd527a6b3 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt @@ -16,6 +16,9 @@ package com.android.settingslib.spa.widget.preference +import androidx.compose.ui.semantics.ProgressBarRangeInfo +import androidx.compose.ui.semantics.SemanticsProperties.ProgressBarRangeInfo +import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText @@ -41,5 +44,20 @@ class SliderPreferenceTest { composeTestRule.onNodeWithText("Slider").assertIsDisplayed() } - // TODO: Add more unit tests for SliderPreference widget. + @Test + fun slider_displayed() { + composeTestRule.setContent { + SliderPreference(object : SliderPreferenceModel { + override val title = "Slider" + override val initValue = 40 + }) + } + + fun progressEqualsTo(progress: Float): SemanticsMatcher = + SemanticsMatcher.expectValue( + ProgressBarRangeInfo, + ProgressBarRangeInfo(progress, 0f..100f, 0) + ) + composeTestRule.onNode(progressEqualsTo(40f)).assertIsDisplayed() + } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt index 1dc52cbfb4ec..99649263c76a 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt @@ -3,6 +3,8 @@ package com.android.settingslib.spaprivileged.framework.common import android.app.admin.DevicePolicyManager import android.app.usage.StorageStatsManager import android.content.Context +import android.content.pm.verify.domain.DomainVerificationManager +import android.os.UserHandle import android.os.UserManager /** The [UserManager] instance. */ @@ -13,3 +15,10 @@ val Context.devicePolicyManager get() = getSystemService(DevicePolicyManager::cl /** The [StorageStatsManager] instance. */ val Context.storageStatsManager get() = getSystemService(StorageStatsManager::class.java)!! + +/** The [DomainVerificationManager] instance. */ +val Context.domainVerificationManager + get() = getSystemService(DomainVerificationManager::class.java)!! + +/** Gets a new [Context] for the given [UserHandle]. */ +fun Context.asUser(userHandle: UserHandle): Context = createContextAsUser(userHandle, 0) diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 3623c7821dfe..edea3abc0618 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -311,4 +311,7 @@ <!-- Whether tilt to bright is enabled by default. --> <bool name="def_wearable_tiltToBrightEnabled">false</bool> + + <!-- Whether vibrate icon is shown in the status bar by default. --> + <integer name="def_statusBarVibrateIconEnabled">0</integer> </resources> diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 3a25d85b5ecf..ccbfac226c46 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -3659,7 +3659,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 210; + private static final int SETTINGS_VERSION = 211; private final int mUserId; @@ -5531,7 +5531,17 @@ public class SettingsProvider extends ContentProvider { // removed now that feature is enabled for everyone currentVersion = 210; } - + if (currentVersion == 210) { + final SettingsState secureSettings = getSecureSettingsLocked(userId); + final int defaultValueVibrateIconEnabled = getContext().getResources() + .getInteger(R.integer.def_statusBarVibrateIconEnabled); + secureSettings.insertSettingOverrideableByRestoreLocked( + Secure.STATUS_BAR_SHOW_VIBRATE_ICON, + String.valueOf(defaultValueVibrateIconEnabled), + null /* tag */, true /* makeDefault */, + SettingsState.SYSTEM_PACKAGE_NAME); + currentVersion = 211; + } // vXXX: Add new settings above this point. if (currentVersion != newVersion) { diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 9747a6c5ca70..aea2f5235201 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -817,7 +817,8 @@ public class SettingsBackupTest { Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, Settings.Secure.ACCESSIBILITY_SHOW_WINDOW_MAGNIFICATION_PROMPT, Settings.Secure.ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT, - Settings.Secure.UI_TRANSLATION_ENABLED); + Settings.Secure.UI_TRANSLATION_ENABLED, + Settings.Secure.CREDENTIAL_SERVICE); @Test public void systemSettingsBackedUpOrDenied() { diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index fecf1241acf8..8acc3e7a1a9a 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -715,6 +715,9 @@ <!-- Permission required for CTS test - ActivityPermissionRationaleTest --> <uses-permission android:name="android.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY" /> + <!-- Permission required for CTS test - CtsDeviceLockTestCases --> + <uses-permission android:name="android.permission.MANAGE_DEVICE_LOCK_STATE" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" android:defaultToDeviceProtectedStorage="true" diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index ca36fa43da76..fdfad2bc2fa1 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -25,7 +25,6 @@ import android.graphics.Rect import android.os.Looper import android.util.Log import android.util.MathUtils -import android.view.GhostView import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT @@ -86,6 +85,9 @@ constructor( */ val sourceIdentity: Any + /** The CUJ associated to this controller. */ + val cuj: DialogCuj? + /** * Move the drawing of the source in the overlay of [viewGroup]. * @@ -142,7 +144,31 @@ constructor( * controlled by this controller. */ // TODO(b/252723237): Make this non-nullable - fun jankConfigurationBuilder(cuj: Int): InteractionJankMonitor.Configuration.Builder? + fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? + + companion object { + /** + * Create a [Controller] that can animate [source] to and from a dialog. + * + * Important: The view must be attached to a [ViewGroup] when calling this function and + * during the animation. For safety, this method will return null when it is not. + * + * Note: The background of [view] should be a (rounded) rectangle so that it can be + * properly animated. + */ + fun fromView(source: View, cuj: DialogCuj? = null): Controller? { + if (source.parent !is ViewGroup) { + Log.e( + TAG, + "Skipping animation as view $source is not attached to a ViewGroup", + Exception(), + ) + return null + } + + return ViewDialogLaunchAnimatorController(source, cuj) + } + } } /** @@ -172,7 +198,12 @@ constructor( cuj: DialogCuj? = null, animateBackgroundBoundsChange: Boolean = false ) { - show(dialog, createController(view), cuj, animateBackgroundBoundsChange) + val controller = Controller.fromView(view, cuj) + if (controller == null) { + dialog.show() + } else { + show(dialog, controller, animateBackgroundBoundsChange) + } } /** @@ -187,10 +218,10 @@ constructor( * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be * made fullscreen and 2 views will be inserted between the dialog DecorView and its children. */ + @JvmOverloads fun show( dialog: Dialog, controller: Controller, - cuj: DialogCuj? = null, animateBackgroundBoundsChange: Boolean = false ) { if (Looper.myLooper() != Looper.getMainLooper()) { @@ -207,7 +238,10 @@ constructor( it.dialog.window.decorView.viewRootImpl == controller.viewRoot } val animateFrom = - animatedParent?.dialogContentWithBackground?.let { createController(it) } ?: controller + animatedParent?.dialogContentWithBackground?.let { + Controller.fromView(it, controller.cuj) + } + ?: controller if (animatedParent == null && animateFrom !is LaunchableView) { // Make sure the View we launch from implements LaunchableView to avoid visibility @@ -244,96 +278,12 @@ constructor( animateBackgroundBoundsChange, animatedParent, isForTesting, - cuj, ) openedDialogs.add(animatedDialog) animatedDialog.start() } - /** Create a [Controller] that can animate [source] to & from a dialog. */ - private fun createController(source: View): Controller { - return object : Controller { - override val viewRoot: ViewRootImpl - get() = source.viewRootImpl - - override val sourceIdentity: Any = source - - override fun startDrawingInOverlayOf(viewGroup: ViewGroup) { - // Create a temporary ghost of the source (which will make it invisible) and add it - // to the host dialog. - GhostView.addGhost(source, viewGroup) - - // The ghost of the source was just created, so the source is currently invisible. - // We need to make sure that it stays invisible as long as the dialog is shown or - // animating. - (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true) - } - - override fun stopDrawingInOverlay() { - // Note: here we should remove the ghost from the overlay, but in practice this is - // already done by the launch controllers created below. - - // Make sure we allow the source to change its visibility again. - (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false) - source.visibility = View.VISIBLE - } - - override fun createLaunchController(): LaunchAnimator.Controller { - val delegate = GhostedViewLaunchAnimatorController(source) - return object : LaunchAnimator.Controller by delegate { - override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { - // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another - // ghost (that ghosts only the source content, and not its background) will - // be added right after this by the delegate and will be animated. - GhostView.removeGhost(source) - delegate.onLaunchAnimationStart(isExpandingFullyAbove) - } - - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { - delegate.onLaunchAnimationEnd(isExpandingFullyAbove) - - // We hide the source when the dialog is showing. We will make this view - // visible again when dismissing the dialog. This does nothing if the source - // implements [LaunchableView], as it's already INVISIBLE in that case. - source.visibility = View.INVISIBLE - } - } - } - - override fun createExitController(): LaunchAnimator.Controller { - return GhostedViewLaunchAnimatorController(source) - } - - override fun shouldAnimateExit(): Boolean { - // The source should be invisible by now, if it's not then something else changed - // its visibility and we probably don't want to run the animation. - if (source.visibility != View.INVISIBLE) { - return false - } - - return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true) - } - - override fun onExitAnimationCancelled() { - // Make sure we allow the source to change its visibility again. - (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false) - - // If the view is invisible it's probably because of us, so we make it visible - // again. - if (source.visibility == View.INVISIBLE) { - source.visibility = View.VISIBLE - } - } - - override fun jankConfigurationBuilder( - cuj: Int - ): InteractionJankMonitor.Configuration.Builder? { - return InteractionJankMonitor.Configuration.Builder.withView(cuj, source) - } - } - } - /** * Launch [dialog] from [another dialog][animateFrom] that was shown using [show]. This will * allow for dismissing the whole stack. @@ -563,9 +513,6 @@ private class AnimatedDialog( * Whether synchronization should be disabled, which can be useful if we are running in a test. */ private val forceDisableSynchronization: Boolean, - - /** Interaction to which the dialog animation is associated. */ - private val cuj: DialogCuj? = null ) { /** * The DecorView of this dialog window. @@ -618,8 +565,9 @@ private class AnimatedDialog( private var hasInstrumentedJank = false fun start() { + val cuj = controller.cuj if (cuj != null) { - val config = controller.jankConfigurationBuilder(cuj.cujType) + val config = controller.jankConfigurationBuilder() if (config != null) { if (cuj.tag != null) { config.setTag(cuj.tag) @@ -917,7 +865,7 @@ private class AnimatedDialog( } if (hasInstrumentedJank) { - interactionJankMonitor.end(cuj!!.cujType) + interactionJankMonitor.end(controller.cuj!!.cujType) } } ) diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt index 8ce372dbb278..40a5e9794d37 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt @@ -30,7 +30,12 @@ interface Expandable { */ fun activityLaunchController(cujType: Int? = null): ActivityLaunchAnimator.Controller? - // TODO(b/230830644): Introduce DialogLaunchAnimator and a function to expose it here. + /** + * Create a [DialogLaunchAnimator.Controller] that can be used to expand this [Expandable] into + * a Dialog, or return `null` if this [Expandable] should not be animated (e.g. if it is + * currently not attached or visible). + */ + fun dialogLaunchController(cuj: DialogCuj? = null): DialogLaunchAnimator.Controller? companion object { /** @@ -39,6 +44,7 @@ interface Expandable { * Note: The background of [view] should be a (rounded) rectangle so that it can be properly * animated. */ + @JvmStatic fun fromView(view: View): Expandable { return object : Expandable { override fun activityLaunchController( @@ -46,6 +52,12 @@ interface Expandable { ): ActivityLaunchAnimator.Controller? { return ActivityLaunchAnimator.Controller.fromView(view, cujType) } + + override fun dialogLaunchController( + cuj: DialogCuj? + ): DialogLaunchAnimator.Controller? { + return DialogLaunchAnimator.Controller.fromView(view, cuj) + } } } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt new file mode 100644 index 000000000000..ecee598afe4e --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation + +import android.view.GhostView +import android.view.View +import android.view.ViewGroup +import android.view.ViewRootImpl +import com.android.internal.jank.InteractionJankMonitor + +/** A [DialogLaunchAnimator.Controller] that can animate a [View] from/to a dialog. */ +class ViewDialogLaunchAnimatorController +internal constructor( + private val source: View, + override val cuj: DialogCuj?, +) : DialogLaunchAnimator.Controller { + override val viewRoot: ViewRootImpl + get() = source.viewRootImpl + + override val sourceIdentity: Any = source + + override fun startDrawingInOverlayOf(viewGroup: ViewGroup) { + // Create a temporary ghost of the source (which will make it invisible) and add it + // to the host dialog. + GhostView.addGhost(source, viewGroup) + + // The ghost of the source was just created, so the source is currently invisible. + // We need to make sure that it stays invisible as long as the dialog is shown or + // animating. + (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true) + } + + override fun stopDrawingInOverlay() { + // Note: here we should remove the ghost from the overlay, but in practice this is + // already done by the launch controllers created below. + + // Make sure we allow the source to change its visibility again. + (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false) + source.visibility = View.VISIBLE + } + + override fun createLaunchController(): LaunchAnimator.Controller { + val delegate = GhostedViewLaunchAnimatorController(source) + return object : LaunchAnimator.Controller by delegate { + override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { + // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another + // ghost (that ghosts only the source content, and not its background) will + // be added right after this by the delegate and will be animated. + GhostView.removeGhost(source) + delegate.onLaunchAnimationStart(isExpandingFullyAbove) + } + + override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + delegate.onLaunchAnimationEnd(isExpandingFullyAbove) + + // We hide the source when the dialog is showing. We will make this view + // visible again when dismissing the dialog. This does nothing if the source + // implements [LaunchableView], as it's already INVISIBLE in that case. + source.visibility = View.INVISIBLE + } + } + } + + override fun createExitController(): LaunchAnimator.Controller { + return GhostedViewLaunchAnimatorController(source) + } + + override fun shouldAnimateExit(): Boolean { + // The source should be invisible by now, if it's not then something else changed + // its visibility and we probably don't want to run the animation. + if (source.visibility != View.INVISIBLE) { + return false + } + + return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true) + } + + override fun onExitAnimationCancelled() { + // Make sure we allow the source to change its visibility again. + (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false) + + // If the view is invisible it's probably because of us, so we make it visible + // again. + if (source.visibility == View.INVISIBLE) { + source.visibility = View.VISIBLE + } + } + + override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? { + val type = cuj?.cujType ?: return null + return InteractionJankMonitor.Configuration.Builder.withView(type, source) + } +} diff --git a/packages/SystemUI/checks/Android.bp b/packages/SystemUI/checks/Android.bp index 9671adde4904..40580d29380b 100644 --- a/packages/SystemUI/checks/Android.bp +++ b/packages/SystemUI/checks/Android.bp @@ -47,6 +47,10 @@ java_test_host { "tests/**/*.kt", "tests/**/*.java", ], + data: [ + ":framework", + ":androidx.annotation_annotation", + ], static_libs: [ "SystemUILintChecker", "junit", diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt index 4eeeb850292a..4b9aa13c0240 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt @@ -32,7 +32,8 @@ import org.jetbrains.uast.UReferenceExpression class SoftwareBitmapDetector : Detector(), SourceCodeScanner { override fun getApplicableReferenceNames(): List<String> { - return mutableListOf("ALPHA_8", "RGB_565", "ARGB_8888", "RGBA_F16", "RGBA_1010102") + return mutableListOf( + "ALPHA_8", "RGB_565", "ARGB_4444", "ARGB_8888", "RGBA_F16", "RGBA_1010102") } override fun visitReference( @@ -40,13 +41,12 @@ class SoftwareBitmapDetector : Detector(), SourceCodeScanner { reference: UReferenceExpression, referenced: PsiElement ) { - val evaluator = context.evaluator if (evaluator.isMemberInClass(referenced as? PsiField, "android.graphics.Bitmap.Config")) { context.report( ISSUE, referenced, - context.getNameLocation(referenced), + context.getNameLocation(reference), "Replace software bitmap with `Config.HARDWARE`" ) } diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt new file mode 100644 index 000000000000..1db072548a76 --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.systemui.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +private const val CLASS_SETTINGS = "android.provider.Settings" + +/** + * Detects usage of static methods in android.provider.Settings and suggests to use an injected + * settings provider instance instead. + */ +@Suppress("UnstableApiUsage") +class StaticSettingsProviderDetector : Detector(), SourceCodeScanner { + override fun getApplicableMethodNames(): List<String> { + return listOf( + "getFloat", + "getInt", + "getLong", + "getString", + "getUriFor", + "putFloat", + "putInt", + "putLong", + "putString" + ) + } + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + val evaluator = context.evaluator + val className = method.containingClass?.qualifiedName + if ( + className != "$CLASS_SETTINGS.Global" && + className != "$CLASS_SETTINGS.Secure" && + className != "$CLASS_SETTINGS.System" + ) { + return + } + if (!evaluator.isStatic(method)) { + return + } + + val subclassName = className.substring(CLASS_SETTINGS.length + 1) + + context.report( + ISSUE, + method, + context.getNameLocation(node), + "`@Inject` a ${subclassName}Settings instead" + ) + } + + companion object { + @JvmField + val ISSUE: Issue = + Issue.create( + id = "StaticSettingsProvider", + briefDescription = "Static settings provider usage", + explanation = + """ + Static settings provider methods, such as `Settings.Global.putInt()`, should \ + not be used because they make testing difficult. Instead, use an injected \ + settings provider. For example, instead of calling `Settings.Secure.getInt()`, \ + annotate the class constructor with `@Inject` and add `SecureSettings` to the \ + parameters. + """, + category = Category.CORRECTNESS, + priority = 8, + severity = Severity.WARNING, + implementation = + Implementation( + StaticSettingsProviderDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt index cf7c1b5e44a2..3f334c1cdb9c 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt @@ -36,6 +36,7 @@ class SystemUIIssueRegistry : IssueRegistry() { RegisterReceiverViaContextDetector.ISSUE, SoftwareBitmapDetector.ISSUE, NonInjectedServiceDetector.ISSUE, + StaticSettingsProviderDetector.ISSUE ) override val api: Int diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt index 486af9dd5d98..141dd0535986 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt @@ -18,6 +18,8 @@ package com.android.internal.systemui.lint import com.android.annotations.NonNull import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java +import com.android.tools.lint.checks.infrastructure.TestFiles.LibraryReferenceTestFile +import java.io.File import org.intellij.lang.annotations.Language @Suppress("UnstableApiUsage") @@ -30,132 +32,8 @@ private fun indentedJava(@NonNull @Language("JAVA") source: String) = java(sourc */ internal val androidStubs = arrayOf( - indentedJava( - """ -package android.app; - -public class ActivityManager { - public static int getCurrentUser() {} -} -""" - ), - indentedJava( - """ -package android.accounts; - -public class AccountManager { - public static AccountManager get(Context context) { return null; } -} -""" - ), - indentedJava( - """ -package android.os; -import android.content.pm.UserInfo; -import android.annotation.UserIdInt; - -public class UserManager { - public UserInfo getUserInfo(@UserIdInt int userId) {} -} -""" - ), - indentedJava(""" -package android.annotation; - -public @interface UserIdInt {} -"""), - indentedJava(""" -package android.content.pm; - -public class UserInfo {} -"""), - indentedJava(""" -package android.os; - -public class Looper {} -"""), - indentedJava(""" -package android.os; - -public class Handler {} -"""), - indentedJava(""" -package android.content; - -public class ServiceConnection {} -"""), - indentedJava(""" -package android.os; - -public enum UserHandle { - ALL -} -"""), - indentedJava( - """ -package android.content; -import android.os.UserHandle; -import android.os.Handler; -import android.os.Looper; -import java.util.concurrent.Executor; - -public class Context { - public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {} - public void registerReceiverAsUser( - BroadcastReceiver receiver, UserHandle user, IntentFilter filter, - String broadcastPermission, Handler scheduler) {} - public void registerReceiverForAllUsers( - BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, - Handler scheduler) {} - public void sendBroadcast(Intent intent) {} - public void sendBroadcast(Intent intent, String receiverPermission) {} - public void sendBroadcastAsUser(Intent intent, UserHandle userHandle, String permission) {} - public void bindService(Intent intent) {} - public void bindServiceAsUser( - Intent intent, ServiceConnection connection, int flags, UserHandle userHandle) {} - public void unbindService(ServiceConnection connection) {} - public Looper getMainLooper() { return null; } - public Executor getMainExecutor() { return null; } - public Handler getMainThreadHandler() { return null; } - public final @Nullable <T> T getSystemService(@NonNull Class<T> serviceClass) { return null; } - public abstract @Nullable Object getSystemService(@ServiceName @NonNull String name); -} -""" - ), - indentedJava( - """ -package android.app; -import android.content.Context; - -public class Activity extends Context {} -""" - ), - indentedJava( - """ -package android.graphics; - -public class Bitmap { - public enum Config { - ARGB_8888, - RGB_565, - HARDWARE - } - public static Bitmap createBitmap(int width, int height, Config config) { - return null; - } -} -""" - ), - indentedJava(""" -package android.content; - -public class BroadcastReceiver {} -"""), - indentedJava(""" -package android.content; - -public class IntentFilter {} -"""), + LibraryReferenceTestFile(File("framework.jar").canonicalFile), + LibraryReferenceTestFile(File("androidx.annotation_annotation.jar").canonicalFile), indentedJava( """ package com.android.systemui.settings; @@ -167,23 +45,4 @@ public interface UserTracker { } """ ), - indentedJava( - """ -package androidx.annotation; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.CONSTRUCTOR; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -@Retention(SOURCE) -@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) -public @interface WorkerThread { -} -""" - ), ) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt index 6ae8fd3f25a1..c35ac61a6543 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt @@ -16,18 +16,15 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class BindServiceOnMainThreadDetectorTest : LintDetectorTest() { +class BindServiceOnMainThreadDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = BindServiceOnMainThreadDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf(BindServiceOnMainThreadDetector.ISSUE) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt index 7d422807ae08..376acb56fac9 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt @@ -16,18 +16,15 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class BroadcastSentViaContextDetectorTest : LintDetectorTest() { +class BroadcastSentViaContextDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = BroadcastSentViaContextDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf(BroadcastSentViaContextDetector.ISSUE) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt index c468af8d09e0..301c338f9b42 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt @@ -16,18 +16,15 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class NonInjectedMainThreadDetectorTest : LintDetectorTest() { +class NonInjectedMainThreadDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = NonInjectedMainThreadDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf(NonInjectedMainThreadDetector.ISSUE) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt index c83a35b46ca6..0a74bfcfee57 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt @@ -16,18 +16,15 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class NonInjectedServiceDetectorTest : LintDetectorTest() { +class NonInjectedServiceDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = NonInjectedServiceDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf(NonInjectedServiceDetector.ISSUE) @Test diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt index ebcddebfbc28..9ed7aa029b1d 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt @@ -16,18 +16,15 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { +class RegisterReceiverViaContextDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = RegisterReceiverViaContextDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf(RegisterReceiverViaContextDetector.ISSUE) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt index b03a11c4f02f..54cac7b35598 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt @@ -16,18 +16,15 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class SlowUserQueryDetectorTest : LintDetectorTest() { +class SlowUserQueryDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = SlowUserQueryDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf( diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt index fb6537e92d15..c632636eb9c8 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt @@ -16,18 +16,15 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class SoftwareBitmapDetectorTest : LintDetectorTest() { +class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = SoftwareBitmapDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf(SoftwareBitmapDetector.ISSUE) @@ -54,12 +51,12 @@ class SoftwareBitmapDetectorTest : LintDetectorTest() { .run() .expect( """ - src/android/graphics/Bitmap.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] - ARGB_8888, - ~~~~~~~~~ - src/android/graphics/Bitmap.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] - RGB_565, - ~~~~~~~ + src/TestClass.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] + Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565); + ~~~~~~~ + src/TestClass.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); + ~~~~~~~~~ 0 errors, 2 warnings """ ) @@ -70,7 +67,7 @@ class SoftwareBitmapDetectorTest : LintDetectorTest() { lint() .files( TestFiles.java( - """ + """ import android.graphics.Bitmap; public class TestClass { @@ -79,8 +76,7 @@ class SoftwareBitmapDetectorTest : LintDetectorTest() { } } """ - ) - .indented(), + ), *stubs ) .issues(SoftwareBitmapDetector.ISSUE) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt new file mode 100644 index 000000000000..b83ed7067bc3 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +@Suppress("UnstableApiUsage") +class StaticSettingsProviderDetectorTest : SystemUILintDetectorTest() { + + override fun getDetector(): Detector = StaticSettingsProviderDetector() + override fun getIssues(): List<Issue> = listOf(StaticSettingsProviderDetector.ISSUE) + + @Test + fun testGetServiceWithString() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + + import android.provider.Settings; + import android.provider.Settings.Global; + import android.provider.Settings.Secure; + + public class TestClass { + public void getSystemServiceWithoutDagger(Context context) { + final ContentResolver cr = mContext.getContentResolver(); + Global.getFloat(cr, Settings.Global.UNLOCK_SOUND); + Global.getInt(cr, Settings.Global.UNLOCK_SOUND); + Global.getLong(cr, Settings.Global.UNLOCK_SOUND); + Global.getString(cr, Settings.Global.UNLOCK_SOUND); + Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f); + Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1); + Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L); + Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1"); + Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f); + Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1); + Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L); + Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1"); + + Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f); + Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1); + Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L); + Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1"); + Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f); + Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1); + Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L); + Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1"); + + Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT); + Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT); + Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT); + Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT); + Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f); + Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1); + Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L); + Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1"); + Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f); + Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1); + Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L); + Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1"); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(StaticSettingsProviderDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:10: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getFloat(cr, Settings.Global.UNLOCK_SOUND); + ~~~~~~~~ + src/test/pkg/TestClass.java:11: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getInt(cr, Settings.Global.UNLOCK_SOUND); + ~~~~~~ + src/test/pkg/TestClass.java:12: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getLong(cr, Settings.Global.UNLOCK_SOUND); + ~~~~~~~ + src/test/pkg/TestClass.java:13: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getString(cr, Settings.Global.UNLOCK_SOUND); + ~~~~~~~~~ + src/test/pkg/TestClass.java:14: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:15: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1); + ~~~~~~ + src/test/pkg/TestClass.java:16: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:17: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:18: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:19: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1); + ~~~~~~ + src/test/pkg/TestClass.java:20: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:21: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:23: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + ~~~~~~~~ + src/test/pkg/TestClass.java:24: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + ~~~~~~ + src/test/pkg/TestClass.java:25: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + ~~~~~~~ + src/test/pkg/TestClass.java:26: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + ~~~~~~~~~ + src/test/pkg/TestClass.java:27: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:28: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1); + ~~~~~~ + src/test/pkg/TestClass.java:29: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:30: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:31: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:32: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1); + ~~~~~~ + src/test/pkg/TestClass.java:33: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:34: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:36: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT); + ~~~~~~~~ + src/test/pkg/TestClass.java:37: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT); + ~~~~~~ + src/test/pkg/TestClass.java:38: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT); + ~~~~~~~ + src/test/pkg/TestClass.java:39: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT); + ~~~~~~~~~ + src/test/pkg/TestClass.java:40: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:41: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1); + ~~~~~~ + src/test/pkg/TestClass.java:42: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:43: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:44: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:45: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1); + ~~~~~~ + src/test/pkg/TestClass.java:46: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:47: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1"); + ~~~~~~~~~ + 0 errors, 36 warnings + """ + ) + } + + private val stubs = androidStubs +} diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt new file mode 100644 index 000000000000..3f93f075fe8b --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt @@ -0,0 +1,48 @@ +package com.android.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestLintTask +import java.io.File +import org.junit.ClassRule +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.model.Statement + +@Suppress("UnstableApiUsage") +@RunWith(JUnit4::class) +abstract class SystemUILintDetectorTest : LintDetectorTest() { + + companion object { + @ClassRule + @JvmField + val libraryChecker: LibraryExists = + LibraryExists("framework.jar", "androidx.annotation_annotation.jar") + } + + class LibraryExists(vararg val libraryNames: String) : TestRule { + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + for (libName in libraryNames) { + val libFile = File(libName) + if (!libFile.canonicalFile.exists()) { + throw Exception( + "Could not find $libName in the test's working directory. " + + "File ${libFile.absolutePath} does not exist." + ) + } + } + base.evaluate() + } + } + } + } + /** + * Customize the lint task to disable SDK usage completely. This ensures that running the tests + * in Android Studio has the same result as running the tests in atest + */ + override fun lint(): TestLintTask = + super.lint().allowMissingSdk(true).sdkHome(File("/dev/null")) +} diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt index 065c3149c2f5..50c3d7e1e76b 100644 --- a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt +++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt @@ -40,17 +40,16 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.animation.LaunchAnimator import kotlin.math.roundToInt -/** A controller that can control animated launches. */ +/** A controller that can control animated launches from an [Expandable]. */ interface ExpandableController { - /** Create an [ActivityLaunchAnimator.Controller] to animate into an Activity. */ - fun forActivity(): ActivityLaunchAnimator.Controller - - /** Create a [DialogLaunchAnimator.Controller] to animate into a Dialog. */ - fun forDialog(): DialogLaunchAnimator.Controller + /** The [Expandable] controlled by this controller. */ + val expandable: Expandable } /** @@ -120,13 +119,26 @@ internal class ExpandableControllerImpl( private val layoutDirection: LayoutDirection, private val isComposed: State<Boolean>, ) : ExpandableController { - override fun forActivity(): ActivityLaunchAnimator.Controller { - return activityController() - } + override val expandable: Expandable = + object : Expandable { + override fun activityLaunchController( + cujType: Int?, + ): ActivityLaunchAnimator.Controller? { + if (!isComposed.value) { + return null + } - override fun forDialog(): DialogLaunchAnimator.Controller { - return dialogController() - } + return activityController(cujType) + } + + override fun dialogLaunchController(cuj: DialogCuj?): DialogLaunchAnimator.Controller? { + if (!isComposed.value) { + return null + } + + return dialogController(cuj) + } + } /** * Create a [LaunchAnimator.Controller] that is going to be used to drive an activity or dialog @@ -233,7 +245,7 @@ internal class ExpandableControllerImpl( } /** Create an [ActivityLaunchAnimator.Controller] that can be used to animate activities. */ - private fun activityController(): ActivityLaunchAnimator.Controller { + private fun activityController(cujType: Int?): ActivityLaunchAnimator.Controller { val delegate = launchController() return object : ActivityLaunchAnimator.Controller, LaunchAnimator.Controller by delegate { override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { @@ -248,10 +260,11 @@ internal class ExpandableControllerImpl( } } - private fun dialogController(): DialogLaunchAnimator.Controller { + private fun dialogController(cuj: DialogCuj?): DialogLaunchAnimator.Controller { return object : DialogLaunchAnimator.Controller { override val viewRoot: ViewRootImpl = composeViewRoot.viewRootImpl override val sourceIdentity: Any = this@ExpandableControllerImpl + override val cuj: DialogCuj? = cuj override fun startDrawingInOverlayOf(viewGroup: ViewGroup) { val newOverlay = viewGroup.overlay as ViewGroupOverlay @@ -294,9 +307,7 @@ internal class ExpandableControllerImpl( isDialogShowing.value = false } - override fun jankConfigurationBuilder( - cuj: Int - ): InteractionJankMonitor.Configuration.Builder? { + override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? { // TODO(b/252723237): Add support for jank monitoring when animating from a // Composable. return null diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt index d0d3052bc544..31ab24748c93 100644 --- a/packages/SystemUI/ktfmt_includes.txt +++ b/packages/SystemUI/ktfmt_includes.txt @@ -832,7 +832,6 @@ -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/WalletControllerImplTest.kt -packages/SystemUI/tests/src/com/android/systemui/statusbar/window/StatusBarWindowStateControllerTest.kt -packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt -packages/SystemUI/tests/src/com/android/systemui/unfold/FoldStateLoggingProviderTest.kt -packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt -packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt index b3dd95553ed0..dee0f5cd1979 100644 --- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt @@ -205,6 +205,13 @@ enum class Style(internal val coreSpec: CoreSpec) { n1 = TonalSpec(HueSource(), ChromaMultiple(0.0833)), n2 = TonalSpec(HueSource(), ChromaMultiple(0.1666)) )), + MONOCHROMATIC(CoreSpec( + a1 = TonalSpec(HueSource(), ChromaConstant(.0)), + a2 = TonalSpec(HueSource(), ChromaConstant(.0)), + a3 = TonalSpec(HueSource(), ChromaConstant(.0)), + n1 = TonalSpec(HueSource(), ChromaConstant(.0)), + n2 = TonalSpec(HueSource(), ChromaConstant(.0)) + )), } class ColorScheme( @@ -219,7 +226,7 @@ class ColorScheme( val neutral1: List<Int> val neutral2: List<Int> - constructor(@ColorInt seed: Int, darkTheme: Boolean): + constructor(@ColorInt seed: Int, darkTheme: Boolean) : this(seed, darkTheme, Style.TONAL_SPOT) @JvmOverloads @@ -227,7 +234,7 @@ class ColorScheme( wallpaperColors: WallpaperColors, darkTheme: Boolean, style: Style = Style.TONAL_SPOT - ): + ) : this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style) val allAccentColors: List<Int> @@ -472,4 +479,4 @@ class ColorScheme( return huePopulation } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp index cafaaf854eed..7709f210f22f 100644 --- a/packages/SystemUI/plugin/Android.bp +++ b/packages/SystemUI/plugin/Android.bp @@ -33,6 +33,7 @@ java_library { static_libs: [ "androidx.annotation_annotation", + "error_prone_annotations", "PluginCoreLib", "SystemUIAnimationLib", ], diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt index 6124e10144f2..6436dcb5f613 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt @@ -14,12 +14,11 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log import android.os.Trace import android.util.Log -import com.android.systemui.log.dagger.LogModule -import com.android.systemui.util.collection.RingBuffer +import com.android.systemui.plugins.util.RingBuffer import com.google.errorprone.annotations.CompileTimeConstant import java.io.PrintWriter import java.util.concurrent.ArrayBlockingQueue @@ -61,15 +60,18 @@ import kotlin.math.max * In either case, `level` can be any of `verbose`, `debug`, `info`, `warn`, `error`, `assert`, or * the first letter of any of the previous. * - * Buffers are provided by [LogModule]. Instances should be created using a [LogBufferFactory]. + * In SystemUI, buffers are provided by LogModule. Instances should be created using a SysUI + * LogBufferFactory. * * @param name The name of this buffer, printed when the buffer is dumped and in some other * situations. * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start - * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches - * the maximum, it behaves like a ring buffer. + * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches the + * maximum, it behaves like a ring buffer. */ -class LogBuffer @JvmOverloads constructor( +class LogBuffer +@JvmOverloads +constructor( private val name: String, private val maxSize: Int, private val logcatEchoTracker: LogcatEchoTracker, @@ -78,7 +80,7 @@ class LogBuffer @JvmOverloads constructor( private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() } private val echoMessageQueue: BlockingQueue<LogMessage>? = - if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null + if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null init { if (logcatEchoTracker.logInBackgroundThread && echoMessageQueue != null) { @@ -133,11 +135,11 @@ class LogBuffer @JvmOverloads constructor( */ @JvmOverloads inline fun log( - tag: String, - level: LogLevel, - messageInitializer: MessageInitializer, - noinline messagePrinter: MessagePrinter, - exception: Throwable? = null, + tag: String, + level: LogLevel, + messageInitializer: MessageInitializer, + noinline messagePrinter: MessagePrinter, + exception: Throwable? = null, ) { val message = obtain(tag, level, messagePrinter, exception) messageInitializer(message) @@ -152,14 +154,13 @@ class LogBuffer @JvmOverloads constructor( * log message is built during runtime, use the [LogBuffer.log] overloaded method that takes in * an initializer and a message printer. * - * Log buffers are limited by the number of entries, so logging more frequently - * will limit the time window that the LogBuffer covers in a bug report. Richer logs, on the - * other hand, make a bug report more actionable, so using the [log] with a messagePrinter to - * add more detail to every log may do more to improve overall logging than adding more logs - * with this method. + * Log buffers are limited by the number of entries, so logging more frequently will limit the + * time window that the LogBuffer covers in a bug report. Richer logs, on the other hand, make a + * bug report more actionable, so using the [log] with a messagePrinter to add more detail to + * every log may do more to improve overall logging than adding more logs with this method. */ fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) = - log(tag, level, {str1 = message}, { str1!! }) + log(tag, level, { str1 = message }, { str1!! }) /** * You should call [log] instead of this method. @@ -172,10 +173,10 @@ class LogBuffer @JvmOverloads constructor( */ @Synchronized fun obtain( - tag: String, - level: LogLevel, - messagePrinter: MessagePrinter, - exception: Throwable? = null, + tag: String, + level: LogLevel, + messagePrinter: MessagePrinter, + exception: Throwable? = null, ): LogMessage { if (!mutable) { return FROZEN_MESSAGE @@ -189,8 +190,7 @@ class LogBuffer @JvmOverloads constructor( * You should call [log] instead of this method. * * After acquiring a message via [obtain], call this method to signal to the buffer that you - * have finished filling in its data fields. The message will be echoed to logcat if - * necessary. + * have finished filling in its data fields. The message will be echoed to logcat if necessary. */ @Synchronized fun commit(message: LogMessage) { @@ -213,7 +213,8 @@ class LogBuffer @JvmOverloads constructor( /** Sends message to echo after determining whether to use Logcat and/or systrace. */ private fun echoToDesiredEndpoints(message: LogMessage) { - val includeInLogcat = logcatEchoTracker.isBufferLoggable(name, message.level) || + val includeInLogcat = + logcatEchoTracker.isBufferLoggable(name, message.level) || logcatEchoTracker.isTagLoggable(message.tag, message.level) echo(message, toLogcat = includeInLogcat, toSystrace = systrace) } @@ -221,7 +222,12 @@ class LogBuffer @JvmOverloads constructor( /** Converts the entire buffer to a newline-delimited string */ @Synchronized fun dump(pw: PrintWriter, tailLength: Int) { - val iterationStart = if (tailLength <= 0) { 0 } else { max(0, buffer.size - tailLength) } + val iterationStart = + if (tailLength <= 0) { + 0 + } else { + max(0, buffer.size - tailLength) + } for (i in iterationStart until buffer.size) { buffer[i].dump(pw) @@ -229,9 +235,9 @@ class LogBuffer @JvmOverloads constructor( } /** - * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called. - * Calls to [log], [obtain], and [commit] will not affect the buffer and will return dummy - * values if necessary. + * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called. Calls + * to [log], [obtain], and [commit] will not affect the buffer and will return dummy values if + * necessary. */ @Synchronized fun freeze() { @@ -241,9 +247,7 @@ class LogBuffer @JvmOverloads constructor( } } - /** - * Undoes the effects of calling [freeze]. - */ + /** Undoes the effects of calling [freeze]. */ @Synchronized fun unfreeze() { if (frozen) { @@ -265,8 +269,11 @@ class LogBuffer @JvmOverloads constructor( } private fun echoToSystrace(message: LogMessage, strMessage: String) { - Trace.instantForTrack(Trace.TRACE_TAG_APP, "UI Events", - "$name - ${message.level.shortString} ${message.tag}: $strMessage") + Trace.instantForTrack( + Trace.TRACE_TAG_APP, + "UI Events", + "$name - ${message.level.shortString} ${message.tag}: $strMessage" + ) } private fun echoToLogcat(message: LogMessage, strMessage: String) { diff --git a/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt index 53f231c9f9d2..b036cf0be1d6 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt @@ -14,17 +14,12 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log import android.util.Log -/** - * Enum version of @Log.Level - */ -enum class LogLevel( - @Log.Level val nativeLevel: Int, - val shortString: String -) { +/** Enum version of @Log.Level */ +enum class LogLevel(@Log.Level val nativeLevel: Int, val shortString: String) { VERBOSE(Log.VERBOSE, "V"), DEBUG(Log.DEBUG, "D"), INFO(Log.INFO, "I"), diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt index dae2592e116c..9468681289bf 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log import java.io.PrintWriter import java.text.SimpleDateFormat @@ -29,9 +29,10 @@ import java.util.Locale * * When a message is logged, the code doing the logging stores data in one or more of the generic * fields ([str1], [int1], etc). When it comes time to dump the message to logcat/bugreport/etc, the - * [messagePrinter] function reads the data stored in the generic fields and converts that to a human- - * readable string. Thus, for every log type there must be a specialized initializer function that - * stores data specific to that log type and a specialized printer function that prints that data. + * [messagePrinter] function reads the data stored in the generic fields and converts that to a + * human- readable string. Thus, for every log type there must be a specialized initializer function + * that stores data specific to that log type and a specialized printer function that prints that + * data. * * See [LogBuffer.log] for more information. */ @@ -55,9 +56,7 @@ interface LogMessage { var bool3: Boolean var bool4: Boolean - /** - * Function that dumps the [LogMessage] to the provided [writer]. - */ + /** Function that dumps the [LogMessage] to the provided [writer]. */ fun dump(writer: PrintWriter) { val formattedTimestamp = DATE_FORMAT.format(timestamp) val shortLevel = level.shortString @@ -68,12 +67,12 @@ interface LogMessage { } /** - * A function that will be called if and when the message needs to be dumped to - * logcat or a bug report. It should read the data stored by the initializer and convert it to - * a human-readable string. The value of `this` will be the LogMessage to be printed. - * **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and NEVER any - * variables in its enclosing scope. Otherwise, the runtime will need to allocate a new instance - * of the printer for each call, thwarting our attempts at avoiding any sort of allocation. + * A function that will be called if and when the message needs to be dumped to logcat or a bug + * report. It should read the data stored by the initializer and convert it to a human-readable + * string. The value of `this` will be the LogMessage to be printed. **IMPORTANT:** The printer + * should ONLY ever reference fields on the LogMessage and NEVER any variables in its enclosing + * scope. Otherwise, the runtime will need to allocate a new instance of the printer for each call, + * thwarting our attempts at avoiding any sort of allocation. */ typealias MessagePrinter = LogMessage.() -> String diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt index 4dd6f652d1c7..f2a6a91adcdf 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt @@ -14,11 +14,9 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log -/** - * Recyclable implementation of [LogMessage]. - */ +/** Recyclable implementation of [LogMessage]. */ data class LogMessageImpl( override var level: LogLevel, override var tag: String, @@ -68,23 +66,24 @@ data class LogMessageImpl( companion object Factory { fun create(): LogMessageImpl { return LogMessageImpl( - LogLevel.DEBUG, - DEFAULT_TAG, - 0, - DEFAULT_PRINTER, - null, - null, - null, - null, - 0, - 0, - 0, - 0, - 0.0, - false, - false, - false, - false) + LogLevel.DEBUG, + DEFAULT_TAG, + 0, + DEFAULT_PRINTER, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0.0, + false, + false, + false, + false + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt index 8cda4236bc87..cfe894f276a0 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt @@ -14,24 +14,16 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log -/** - * Keeps track of which [LogBuffer] messages should also appear in logcat. - */ +/** Keeps track of which [LogBuffer] messages should also appear in logcat. */ interface LogcatEchoTracker { - /** - * Whether [bufferName] should echo messages of [level] or higher to logcat. - */ + /** Whether [bufferName] should echo messages of [level] or higher to logcat. */ fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean - /** - * Whether [tagName] should echo messages of [level] or higher to logcat. - */ + /** Whether [tagName] should echo messages of [level] or higher to logcat. */ fun isTagLoggable(tagName: String, level: LogLevel): Boolean - /** - * Whether to log messages in a background thread. - */ + /** Whether to log messages in a background thread. */ val logInBackgroundThread: Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt index 40b0cdc173d8..d3fabaccb6d3 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log import android.content.ContentResolver import android.database.ContentObserver @@ -36,19 +36,15 @@ import android.provider.Settings * $ adb shell settings put global systemui/tag/<tag> <level> * ``` */ -class LogcatEchoTrackerDebug private constructor( - private val contentResolver: ContentResolver -) : LogcatEchoTracker { +class LogcatEchoTrackerDebug private constructor(private val contentResolver: ContentResolver) : + LogcatEchoTracker { private val cachedBufferLevels: MutableMap<String, LogLevel> = mutableMapOf() private val cachedTagLevels: MutableMap<String, LogLevel> = mutableMapOf() override val logInBackgroundThread = true companion object Factory { @JvmStatic - fun create( - contentResolver: ContentResolver, - mainLooper: Looper - ): LogcatEchoTrackerDebug { + fun create(contentResolver: ContentResolver, mainLooper: Looper): LogcatEchoTrackerDebug { val tracker = LogcatEchoTrackerDebug(contentResolver) tracker.attach(mainLooper) return tracker @@ -57,37 +53,35 @@ class LogcatEchoTrackerDebug private constructor( private fun attach(mainLooper: Looper) { contentResolver.registerContentObserver( - Settings.Global.getUriFor(BUFFER_PATH), - true, - object : ContentObserver(Handler(mainLooper)) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - super.onChange(selfChange, uri) - cachedBufferLevels.clear() - } - }) + Settings.Global.getUriFor(BUFFER_PATH), + true, + object : ContentObserver(Handler(mainLooper)) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + cachedBufferLevels.clear() + } + } + ) contentResolver.registerContentObserver( - Settings.Global.getUriFor(TAG_PATH), - true, - object : ContentObserver(Handler(mainLooper)) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - super.onChange(selfChange, uri) - cachedTagLevels.clear() - } - }) + Settings.Global.getUriFor(TAG_PATH), + true, + object : ContentObserver(Handler(mainLooper)) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + cachedTagLevels.clear() + } + } + ) } - /** - * Whether [bufferName] should echo messages of [level] or higher to logcat. - */ + /** Whether [bufferName] should echo messages of [level] or higher to logcat. */ @Synchronized override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean { return level.ordinal >= getLogLevel(bufferName, BUFFER_PATH, cachedBufferLevels).ordinal } - /** - * Whether [tagName] should echo messages of [level] or higher to logcat. - */ + /** Whether [tagName] should echo messages of [level] or higher to logcat. */ @Synchronized override fun isTagLoggable(tagName: String, level: LogLevel): Boolean { return level >= getLogLevel(tagName, TAG_PATH, cachedTagLevels) diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt index 1a4ad1907ff1..3c8bda4a44e0 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt @@ -14,11 +14,9 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log -/** - * Production version of [LogcatEchoTracker] that isn't configurable. - */ +/** Production version of [LogcatEchoTracker] that isn't configurable. */ class LogcatEchoTrackerProd : LogcatEchoTracker { override val logInBackgroundThread = false diff --git a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt index 97dc842ec699..68d78907f028 100644 --- a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.util.collection +package com.android.systemui.plugins.util import kotlin.math.max @@ -32,19 +32,16 @@ import kotlin.math.max * @param factory A function that creates a fresh instance of T. Used by the buffer while it's * growing to [maxSize]. */ -class RingBuffer<T>( - private val maxSize: Int, - private val factory: () -> T -) : Iterable<T> { +class RingBuffer<T>(private val maxSize: Int, private val factory: () -> T) : Iterable<T> { private val buffer = MutableList<T?>(maxSize) { null } /** * An abstract representation that points to the "end" of the buffer. Increments every time - * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into - * the backing array. Always points to the "next" available slot in the buffer. Before the - * buffer has completely filled, the value pointed to will be null. Afterward, it will be the - * value at the "beginning" of the buffer. + * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into the + * backing array. Always points to the "next" available slot in the buffer. Before the buffer + * has completely filled, the value pointed to will be null. Afterward, it will be the value at + * the "beginning" of the buffer. * * This value is unlikely to overflow. Assuming [advance] is called at rate of 100 calls/ms, * omega will overflow after a little under three million years of continuous operation. @@ -60,24 +57,23 @@ class RingBuffer<T>( /** * Advances the buffer's position by one and returns the value that is now present at the "end" - * of the buffer. If the buffer is not yet full, uses [factory] to create a new item. - * Otherwise, reuses the value that was previously at the "beginning" of the buffer. + * of the buffer. If the buffer is not yet full, uses [factory] to create a new item. Otherwise, + * reuses the value that was previously at the "beginning" of the buffer. * - * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that - * was previously stored on it. + * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that was + * previously stored on it. */ fun advance(): T { val index = indexOf(omega) omega += 1 - val entry = buffer[index] ?: factory().also { - buffer[index] = it - } + val entry = buffer[index] ?: factory().also { buffer[index] = it } return entry } /** * Returns the value stored at [index], which can range from 0 (the "start", or oldest element - * of the buffer) to [size] - 1 (the "end", or newest element of the buffer). + * of the buffer) to [size] + * - 1 (the "end", or newest element of the buffer). */ operator fun get(index: Int): T { if (index < 0 || index >= size) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt index 56aff3c2fc8b..a39b856f0f49 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt +++ b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt @@ -2,6 +2,7 @@ package com.android.systemui.log import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.log.LogBuffer import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter @@ -18,8 +19,7 @@ class LogBufferTest : SysuiTestCase() { private lateinit var outputWriter: StringWriter - @Mock - private lateinit var logcatEchoTracker: LogcatEchoTracker + @Mock private lateinit var logcatEchoTracker: LogcatEchoTracker @Before fun setup() { @@ -67,15 +67,17 @@ class LogBufferTest : SysuiTestCase() { @Test fun dump_writesCauseAndStacktrace() { buffer = createBuffer() - val exception = createTestException("Exception message", + val exception = + createTestException( + "Exception message", "TestClass", - cause = createTestException("The real cause!", "TestClass")) + cause = createTestException("The real cause!", "TestClass") + ) buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception) val dumpedString = dumpBuffer() - assertThat(dumpedString) - .contains("Caused by: java.lang.RuntimeException: The real cause!") + assertThat(dumpedString).contains("Caused by: java.lang.RuntimeException: The real cause!") assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:1)") assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:2)") } @@ -85,49 +87,47 @@ class LogBufferTest : SysuiTestCase() { buffer = createBuffer() val exception = RuntimeException("Root exception message") exception.addSuppressed( - createTestException( - "First suppressed exception", - "FirstClass", - createTestException("Cause of suppressed exp", "ThirdClass") - )) - exception.addSuppressed( - createTestException("Second suppressed exception", "SecondClass")) + createTestException( + "First suppressed exception", + "FirstClass", + createTestException("Cause of suppressed exp", "ThirdClass") + ) + ) + exception.addSuppressed(createTestException("Second suppressed exception", "SecondClass")) buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception) val dumpedStr = dumpBuffer() // first suppressed exception assertThat(dumpedStr) - .contains("Suppressed: " + - "java.lang.RuntimeException: First suppressed exception") + .contains("Suppressed: " + "java.lang.RuntimeException: First suppressed exception") assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:1)") assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:2)") assertThat(dumpedStr) - .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp") + .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp") assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:1)") assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:2)") // second suppressed exception assertThat(dumpedStr) - .contains("Suppressed: " + - "java.lang.RuntimeException: Second suppressed exception") + .contains("Suppressed: " + "java.lang.RuntimeException: Second suppressed exception") assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:1)") assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:2)") } private fun createTestException( - message: String, - errorClass: String, - cause: Throwable? = null, + message: String, + errorClass: String, + cause: Throwable? = null, ): Exception { val exception = RuntimeException(message, cause) - exception.stackTrace = (1..5).map { lineNumber -> - StackTraceElement(errorClass, - "TestMethod", - "$errorClass.java", - lineNumber) - }.toTypedArray() + exception.stackTrace = + (1..5) + .map { lineNumber -> + StackTraceElement(errorClass, "TestMethod", "$errorClass.java", lineNumber) + } + .toTypedArray() return exception } diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml index bc8e540cb612..3bcc37a478c9 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml @@ -16,45 +16,47 @@ <com.android.systemui.biometrics.AuthCredentialPasswordView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" - android:elevation="@dimen/biometric_dialog_elevation"> + android:elevation="@dimen/biometric_dialog_elevation" + android:theme="?app:attr/lockPinPasswordStyle"> <RelativeLayout android:id="@+id/auth_credential_header" - style="@style/AuthCredentialHeaderStyle" + style="?headerStyle" android:layout_width="wrap_content" android:layout_height="match_parent"> <ImageView android:id="@+id/icon" - style="@style/TextAppearance.AuthNonBioCredential.Icon" + style="?headerIconStyle" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:contentDescription="@null"/> <TextView android:id="@+id/title" - style="@style/TextAppearance.AuthNonBioCredential.Title" + style="?titleTextAppearance" android:layout_below="@id/icon" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/subtitle" - style="@style/TextAppearance.AuthNonBioCredential.Subtitle" + style="?subTitleTextAppearance" android:layout_below="@id/title" android:layout_alignParentLeft="true" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/description" - style="@style/TextAppearance.AuthNonBioCredential.Description" + style="?descriptionTextAppearance" android:layout_below="@id/subtitle" android:layout_alignParentLeft="true" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" /> </RelativeLayout> @@ -67,7 +69,7 @@ <ImeAwareEditText android:id="@+id/lockPassword" - style="@style/TextAppearance.AuthCredential.PasswordEntry" + style="?passwordTextAppearance" android:layout_width="208dp" android:layout_height="wrap_content" android:layout_gravity="center" @@ -77,7 +79,7 @@ <TextView android:id="@+id/error" - style="@style/TextAppearance.AuthNonBioCredential.Error" + style="?errorTextAppearance" android:layout_gravity="center" android:layout_width="wrap_content" android:layout_height="wrap_content" /> diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml index 19a85fec1397..a3dd334bd667 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml @@ -16,91 +16,71 @@ <com.android.systemui.biometrics.AuthCredentialPatternView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" - android:elevation="@dimen/biometric_dialog_elevation"> + android:elevation="@dimen/biometric_dialog_elevation" + android:theme="?app:attr/lockPatternStyle"> - <LinearLayout + <RelativeLayout + android:id="@+id/auth_credential_header" + style="?headerStyle" android:layout_width="0dp" android:layout_height="match_parent" - android:layout_weight="1" - android:gravity="center" - android:orientation="vertical"> - - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_weight="1"/> + android:layout_weight="1"> <ImageView android:id="@+id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + style="?headerIconStyle" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:contentDescription="@null"/> <TextView android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Title"/> + style="?titleTextAppearance" + android:layout_below="@id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> <TextView android:id="@+id/subtitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Subtitle"/> + style="?subTitleTextAppearance" + android:layout_below="@id/title" + android:layout_alignParentLeft="true" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> <TextView android:id="@+id/description" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Description"/> + style="?descriptionTextAppearance" + android:layout_below="@id/subtitle" + android:layout_alignParentLeft="true" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_weight="1"/> + </RelativeLayout> + + <FrameLayout + android:layout_weight="1" + style="?containerStyle" + android:layout_width="0dp" + android:layout_height="match_parent"> + + <com.android.internal.widget.LockPatternView + android:id="@+id/lockPattern" + android:layout_gravity="center" + android:layout_width="match_parent" + android:layout_height="match_parent"/> <TextView android:id="@+id/error" + style="?errorTextAppearance" android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Error"/> - - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_weight="1"/> - - </LinearLayout> - - <LinearLayout - android:layout_width="0dp" - android:layout_height="match_parent" - android:layout_weight="1" - android:orientation="vertical" - android:gravity="center" - android:paddingLeft="0dp" - android:paddingRight="0dp" - android:paddingTop="0dp" - android:paddingBottom="16dp" - android:clipToPadding="false"> - - <FrameLayout - android:layout_width="wrap_content" - android:layout_height="0dp" - android:layout_weight="1" - style="@style/LockPatternContainerStyle"> - - <com.android.internal.widget.LockPatternView - android:id="@+id/lockPattern" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - style="@style/LockPatternStyleBiometricPrompt"/> - - </FrameLayout> + android:layout_gravity="center_horizontal|bottom"/> - </LinearLayout> + </FrameLayout> </com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_credential_password_view.xml b/packages/SystemUI/res/layout/auth_credential_password_view.xml index 75a80bc39a1f..774b335f913e 100644 --- a/packages/SystemUI/res/layout/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml @@ -16,43 +16,45 @@ <com.android.systemui.biometrics.AuthCredentialPasswordView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:elevation="@dimen/biometric_dialog_elevation" - android:orientation="vertical"> + android:orientation="vertical" + android:theme="?app:attr/lockPinPasswordStyle"> <RelativeLayout android:id="@+id/auth_credential_header" - style="@style/AuthCredentialHeaderStyle" + style="?headerStyle" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/icon" - style="@style/TextAppearance.AuthNonBioCredential.Icon" + style="?headerIconStyle" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:contentDescription="@null"/> <TextView android:id="@+id/title" - style="@style/TextAppearance.AuthNonBioCredential.Title" + style="?titleTextAppearance" android:layout_below="@id/icon" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content"/> <TextView android:id="@+id/subtitle" - style="@style/TextAppearance.AuthNonBioCredential.Subtitle" + style="?subTitleTextAppearance" android:layout_below="@id/title" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content"/> <TextView android:id="@+id/description" - style="@style/TextAppearance.AuthNonBioCredential.Description" + style="?descriptionTextAppearance" android:layout_below="@id/subtitle" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content"/> </RelativeLayout> @@ -64,7 +66,7 @@ <ImeAwareEditText android:id="@+id/lockPassword" - style="@style/TextAppearance.AuthCredential.PasswordEntry" + style="?passwordTextAppearance" android:layout_width="208dp" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" @@ -74,7 +76,7 @@ <TextView android:id="@+id/error" - style="@style/TextAppearance.AuthNonBioCredential.Error" + style="?errorTextAppearance" android:layout_gravity="center_horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" /> diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml index dada9813c320..4af997017bba 100644 --- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml @@ -16,87 +16,66 @@ <com.android.systemui.biometrics.AuthCredentialPatternView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - android:gravity="center_horizontal" - android:elevation="@dimen/biometric_dialog_elevation"> + android:elevation="@dimen/biometric_dialog_elevation" + android:theme="?app:attr/lockPatternStyle"> <RelativeLayout + android:id="@+id/auth_credential_header" + style="?headerStyle" android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - - <LinearLayout - android:id="@+id/auth_credential_header" - style="@style/AuthCredentialHeaderStyle" - android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <ImageView - android:id="@+id/icon" - android:layout_width="48dp" - android:layout_height="48dp" - android:contentDescription="@null" /> - - <TextView - android:id="@+id/title" - style="@style/TextAppearance.AuthNonBioCredential.Title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - - <TextView - android:id="@+id/subtitle" - style="@style/TextAppearance.AuthNonBioCredential.Subtitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/icon" + style="?headerIconStyle" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:contentDescription="@null"/> + + <TextView + android:id="@+id/title" + style="?titleTextAppearance" + android:layout_below="@id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <TextView + android:id="@+id/subtitle" + style="?subTitleTextAppearance" + android:layout_below="@id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <TextView + android:id="@+id/description" + style="?descriptionTextAppearance" + android:layout_below="@id/subtitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + </RelativeLayout> - <TextView - android:id="@+id/description" - style="@style/TextAppearance.AuthNonBioCredential.Description" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - </LinearLayout> + <FrameLayout + android:id="@+id/auth_credential_container" + style="?containerStyle" + android:layout_width="match_parent" + android:layout_height="match_parent"> - <LinearLayout + <com.android.internal.widget.LockPatternView + android:id="@+id/lockPattern" + android:layout_gravity="center" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/auth_credential_header" - android:gravity="center" - android:orientation="vertical" - android:paddingBottom="16dp" - android:paddingTop="60dp"> + android:layout_height="match_parent"/> - <FrameLayout - style="@style/LockPatternContainerStyle" - android:layout_width="wrap_content" - android:layout_height="0dp" - android:layout_weight="1"> - - <com.android.internal.widget.LockPatternView - android:id="@+id/lockPattern" - style="@style/LockPatternStyle" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" /> - - </FrameLayout> - - </LinearLayout> - - <LinearLayout + <TextView + android:id="@+id/error" + style="?errorTextAppearance" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentBottom="true"> - - <TextView - android:id="@+id/error" - style="@style/TextAppearance.AuthNonBioCredential.Error" - android:layout_width="match_parent" - android:layout_height="wrap_content" /> - - </LinearLayout> - - </RelativeLayout> + android:layout_gravity="center_horizontal|bottom"/> + </FrameLayout> </com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml index 4da77118f00b..bc97e511e7f4 100644 --- a/packages/SystemUI/res/layout/chipbar.xml +++ b/packages/SystemUI/res/layout/chipbar.xml @@ -19,12 +19,12 @@ <com.android.systemui.temporarydisplay.chipbar.ChipbarRootView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:id="@+id/media_ttt_sender_chip" + android:id="@+id/chipbar_root_view" android:layout_width="wrap_content" android:layout_height="wrap_content"> <LinearLayout - android:id="@+id/media_ttt_sender_chip_inner" + android:id="@+id/chipbar_inner" android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -39,7 +39,7 @@ > <com.android.internal.widget.CachingIconView - android:id="@+id/app_icon" + android:id="@+id/start_icon" android:layout_width="@dimen/media_ttt_app_icon_size" android:layout_height="@dimen/media_ttt_app_icon_size" android:layout_marginEnd="12dp" @@ -69,7 +69,7 @@ /> <ImageView - android:id="@+id/failure_icon" + android:id="@+id/error" android:layout_width="@dimen/media_ttt_status_icon_size" android:layout_height="@dimen/media_ttt_status_icon_size" android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" @@ -78,11 +78,11 @@ android:alpha="0.0" /> + <!-- TODO(b/245610654): Re-name all the media-specific dimens to chipbar dimens instead. --> <TextView - android:id="@+id/undo" + android:id="@+id/end_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/media_transfer_undo" android:textColor="?androidprv:attr/textColorOnAccent" android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" android:textSize="@dimen/media_ttt_text_size" diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml index 5dc34b9db594..a565988c14ad 100644 --- a/packages/SystemUI/res/layout/combined_qs_header.xml +++ b/packages/SystemUI/res/layout/combined_qs_header.xml @@ -73,8 +73,8 @@ android:singleLine="true" android:textDirection="locale" android:textAppearance="@style/TextAppearance.QS.Status" - android:transformPivotX="0sp" - android:transformPivotY="20sp" + android:transformPivotX="0dp" + android:transformPivotY="24dp" android:scaleX="1" android:scaleY="1" /> diff --git a/packages/SystemUI/res/values-land/styles.xml b/packages/SystemUI/res/values-land/styles.xml index ac9a947f4417..aefd9981d02e 100644 --- a/packages/SystemUI/res/values-land/styles.xml +++ b/packages/SystemUI/res/values-land/styles.xml @@ -24,7 +24,36 @@ <item name="android:paddingEnd">24dp</item> <item name="android:paddingTop">48dp</item> <item name="android:paddingBottom">10dp</item> - <item name="android:gravity">top|center_horizontal</item> + <item name="android:gravity">top|left</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">320dp</item> + <item name="android:maxWidth">320dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">60dp</item> + <item name="android:paddingVertical">20dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">6dp</item> + <item name="android:textSize">36dp</item> + <item name="android:focusable">true</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Subtitle"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">6dp</item> + <item name="android:textSize">18sp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Description"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">6dp</item> + <item name="android:textSize">18sp</item> </style> </resources> diff --git a/packages/SystemUI/res/values-sw600dp-land/styles.xml b/packages/SystemUI/res/values-sw600dp-land/styles.xml new file mode 100644 index 000000000000..8148d3dfaf7d --- /dev/null +++ b/packages/SystemUI/res/values-sw600dp-land/styles.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">120dp</item> + <item name="android:paddingVertical">40dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Subtitle"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Description"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> +</resources> diff --git a/packages/SystemUI/res/values-sw600dp-port/styles.xml b/packages/SystemUI/res/values-sw600dp-port/styles.xml new file mode 100644 index 000000000000..771de08cb360 --- /dev/null +++ b/packages/SystemUI/res/values-sw600dp-port/styles.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialHeaderStyle"> + <item name="android:paddingStart">120dp</item> + <item name="android:paddingEnd">120dp</item> + <item name="android:paddingTop">80dp</item> + <item name="android:paddingBottom">10dp</item> + <item name="android:layout_gravity">top</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">180dp</item> + <item name="android:paddingVertical">80dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">24dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + +</resources> diff --git a/packages/SystemUI/res/values-sw720dp-land/styles.xml b/packages/SystemUI/res/values-sw720dp-land/styles.xml new file mode 100644 index 000000000000..f9ed67d89de7 --- /dev/null +++ b/packages/SystemUI/res/values-sw720dp-land/styles.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">120dp</item> + <item name="android:paddingVertical">40dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Subtitle"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Description"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> + +</resources> diff --git a/packages/SystemUI/res/values-sw720dp-port/styles.xml b/packages/SystemUI/res/values-sw720dp-port/styles.xml new file mode 100644 index 000000000000..78d299c483e6 --- /dev/null +++ b/packages/SystemUI/res/values-sw720dp-port/styles.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialHeaderStyle"> + <item name="android:paddingStart">120dp</item> + <item name="android:paddingEnd">120dp</item> + <item name="android:paddingTop">80dp</item> + <item name="android:paddingBottom">10dp</item> + <item name="android:layout_gravity">top</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">240dp</item> + <item name="android:paddingVertical">120dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">24dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + +</resources> diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml index 9a71995383ac..df0659d67afe 100644 --- a/packages/SystemUI/res/values/attrs.xml +++ b/packages/SystemUI/res/values/attrs.xml @@ -191,5 +191,18 @@ <declare-styleable name="DelayableMarqueeTextView"> <attr name="marqueeDelay" format="integer" /> </declare-styleable> + + <declare-styleable name="AuthCredentialView"> + <attr name="lockPatternStyle" format="reference" /> + <attr name="lockPinPasswordStyle" format="reference" /> + <attr name="containerStyle" format="reference" /> + <attr name="headerStyle" format="reference" /> + <attr name="headerIconStyle" format="reference" /> + <attr name="titleTextAppearance" format="reference" /> + <attr name="subTitleTextAppearance" format="reference" /> + <attr name="descriptionTextAppearance" format="reference" /> + <attr name="passwordTextAppearance" format="reference" /> + <attr name="errorTextAppearance" format="reference"/> + </declare-styleable> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 01c9ac1b9d15..66f0e7543469 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -519,7 +519,7 @@ <dimen name="qs_tile_margin_horizontal">8dp</dimen> <dimen name="qs_tile_margin_vertical">@dimen/qs_tile_margin_horizontal</dimen> <dimen name="qs_tile_margin_top_bottom">4dp</dimen> - <dimen name="qs_brightness_margin_top">8dp</dimen> + <dimen name="qs_brightness_margin_top">12dp</dimen> <dimen name="qs_brightness_margin_bottom">16dp</dimen> <dimen name="qqs_layout_margin_top">16dp</dimen> <dimen name="qqs_layout_padding_bottom">24dp</dimen> @@ -572,6 +572,7 @@ <dimen name="qs_header_row_min_height">48dp</dimen> <dimen name="qs_header_non_clickable_element_height">24dp</dimen> + <dimen name="new_qs_header_non_clickable_element_height">20dp</dimen> <dimen name="qs_footer_padding">20dp</dimen> <dimen name="qs_security_footer_height">88dp</dimen> diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml index 3164ed1e6751..e30d4415a0c4 100644 --- a/packages/SystemUI/res/values/integers.xml +++ b/packages/SystemUI/res/values/integers.xml @@ -28,4 +28,11 @@ <!-- The time it takes for the over scroll release animation to complete, in milli seconds. --> <integer name="lockscreen_shade_over_scroll_release_duration">0</integer> + + <!-- Values for transition of QS Headers --> + <integer name="fade_out_complete_frame">14</integer> + <integer name="fade_in_start_frame">58</integer> + <!-- Percentage of displacement for items in QQS to guarantee matching with bottom of clock at + fade_out_complete_frame --> + <dimen name="percent_displacement_at_fade_out" format="float">0.1066</dimen> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index a734fa744b48..e76887babc50 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -128,11 +128,10 @@ <!-- This is hard coded to be sans-serif-condensed to match the icons --> <style name="TextAppearance.QS.Status"> - <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item> + <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> <item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textSize">14sp</item> <item name="android:letterSpacing">0.01</item> - <item name="android:lineHeight">20sp</item> </style> <style name="TextAppearance.QS.SecurityFooter" parent="@style/TextAppearance.QS.Status"> @@ -143,12 +142,10 @@ <style name="TextAppearance.QS.Status.Carriers" /> <style name="TextAppearance.QS.Status.Carriers.NoCarrierText"> - <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item> <item name="android:textColor">?android:attr/textColorSecondary</item> </style> <style name="TextAppearance.QS.Status.Build"> - <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item> <item name="android:textColor">?android:attr/textColorSecondary</item> </style> @@ -198,15 +195,11 @@ <item name="android:textColor">?android:attr/textColorPrimary</item> </style> - <style name="TextAppearance.AuthNonBioCredential.Icon"> - <item name="android:layout_width">@dimen/biometric_auth_icon_size</item> - <item name="android:layout_height">@dimen/biometric_auth_icon_size</item> - </style> - <style name="TextAppearance.AuthNonBioCredential.Title"> <item name="android:fontFamily">google-sans</item> - <item name="android:layout_marginTop">20dp</item> - <item name="android:textSize">36sp</item> + <item name="android:layout_marginTop">24dp</item> + <item name="android:textSize">36dp</item> + <item name="android:focusable">true</item> </style> <style name="TextAppearance.AuthNonBioCredential.Subtitle"> @@ -218,12 +211,10 @@ <style name="TextAppearance.AuthNonBioCredential.Description"> <item name="android:fontFamily">google-sans</item> <item name="android:layout_marginTop">20dp</item> - <item name="android:textSize">16sp</item> + <item name="android:textSize">18sp</item> </style> <style name="TextAppearance.AuthNonBioCredential.Error"> - <item name="android:paddingTop">6dp</item> - <item name="android:paddingBottom">18dp</item> <item name="android:paddingHorizontal">24dp</item> <item name="android:textSize">14sp</item> <item name="android:textColor">?android:attr/colorError</item> @@ -242,12 +233,33 @@ <style name="AuthCredentialHeaderStyle"> <item name="android:paddingStart">48dp</item> <item name="android:paddingEnd">48dp</item> - <item name="android:paddingTop">28dp</item> - <item name="android:paddingBottom">20dp</item> - <item name="android:orientation">vertical</item> + <item name="android:paddingTop">48dp</item> + <item name="android:paddingBottom">10dp</item> <item name="android:layout_gravity">top</item> </style> + <style name="AuthCredentialIconStyle"> + <item name="android:layout_width">@dimen/biometric_auth_icon_size</item> + <item name="android:layout_height">@dimen/biometric_auth_icon_size</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:padding">20dp</item> + </style> + + <style name="AuthCredentialPinPasswordContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">48dp</item> + <item name="android:maxWidth">600dp</item> + <item name="android:minHeight">48dp</item> + <item name="android:minWidth">200dp</item> + </style> + <style name="DeviceManagementDialogTitle"> <item name="android:gravity">center</item> <item name="android:textAppearance">@style/TextAppearance.DeviceManagementDialog.Title</item> @@ -285,7 +297,9 @@ <item name="wallpaperTextColorSecondary">@*android:color/secondary_text_material_dark</item> <item name="wallpaperTextColorAccent">@color/material_dynamic_primary90</item> <item name="android:colorError">@*android:color/error_color_material_dark</item> - <item name="*android:lockPatternStyle">@style/LockPatternStyle</item> + <item name="*android:lockPatternStyle">@style/LockPatternViewStyle</item> + <item name="lockPatternStyle">@style/LockPatternContainerStyle</item> + <item name="lockPinPasswordStyle">@style/LockPinPasswordContainerStyle</item> <item name="passwordStyle">@style/PasswordTheme</item> <item name="numPadKeyStyle">@style/NumPadKey</item> <item name="backgroundProtectedStyle">@style/BackgroundProtectedStyle</item> @@ -311,27 +325,33 @@ <item name="android:textColor">?attr/wallpaperTextColor</item> </style> - <style name="LockPatternContainerStyle"> - <item name="android:maxHeight">400dp</item> - <item name="android:maxWidth">420dp</item> - <item name="android:minHeight">0dp</item> - <item name="android:minWidth">0dp</item> - <item name="android:paddingHorizontal">60dp</item> - <item name="android:paddingBottom">40dp</item> + <style name="AuthCredentialStyle"> + <item name="*android:regularColor">?android:attr/colorForeground</item> + <item name="*android:successColor">?android:attr/colorForeground</item> + <item name="*android:errorColor">?android:attr/colorError</item> + <item name="*android:dotColor">?android:attr/textColorSecondary</item> + <item name="headerStyle">@style/AuthCredentialHeaderStyle</item> + <item name="headerIconStyle">@style/AuthCredentialIconStyle</item> + <item name="titleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Title</item> + <item name="subTitleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Subtitle</item> + <item name="descriptionTextAppearance">@style/TextAppearance.AuthNonBioCredential.Description</item> + <item name="passwordTextAppearance">@style/TextAppearance.AuthCredential.PasswordEntry</item> + <item name="errorTextAppearance">@style/TextAppearance.AuthNonBioCredential.Error</item> </style> - <style name="LockPatternStyle"> + <style name="LockPatternViewStyle" > <item name="*android:regularColor">?android:attr/colorAccent</item> <item name="*android:successColor">?android:attr/textColorPrimary</item> <item name="*android:errorColor">?android:attr/colorError</item> <item name="*android:dotColor">?android:attr/textColorSecondary</item> </style> - <style name="LockPatternStyleBiometricPrompt"> - <item name="*android:regularColor">?android:attr/colorForeground</item> - <item name="*android:successColor">?android:attr/colorForeground</item> - <item name="*android:errorColor">?android:attr/colorError</item> - <item name="*android:dotColor">?android:attr/textColorSecondary</item> + <style name="LockPatternContainerStyle" parent="@style/AuthCredentialStyle"> + <item name="containerStyle">@style/AuthCredentialPatternContainerStyle</item> + </style> + + <style name="LockPinPasswordContainerStyle" parent="@style/AuthCredentialStyle"> + <item name="containerStyle">@style/AuthCredentialPinPasswordContainerStyle</item> </style> <style name="Theme.SystemUI.QuickSettings" parent="@*android:style/Theme.DeviceDefault"> diff --git a/packages/SystemUI/res/xml/combined_qs_header_scene.xml b/packages/SystemUI/res/xml/combined_qs_header_scene.xml index f3866c08cbfc..de855e275f5f 100644 --- a/packages/SystemUI/res/xml/combined_qs_header_scene.xml +++ b/packages/SystemUI/res/xml/combined_qs_header_scene.xml @@ -27,67 +27,60 @@ <KeyPosition app:keyPositionType="deltaRelative" app:percentX="0" - app:percentY="0" - app:framePosition="49" + app:percentY="@dimen/percent_displacement_at_fade_out" + app:framePosition="@integer/fade_out_complete_frame" app:sizePercent="0" app:curveFit="linear" app:motionTarget="@id/date" /> <KeyPosition app:keyPositionType="deltaRelative" app:percentX="1" - app:percentY="0.51" + app:percentY="0.5" app:sizePercent="1" - app:framePosition="51" + app:framePosition="50" app:curveFit="linear" app:motionTarget="@id/date" /> <KeyAttribute app:motionTarget="@id/date" - app:framePosition="30" + app:framePosition="14" android:alpha="0" /> <KeyAttribute app:motionTarget="@id/date" - app:framePosition="70" + app:framePosition="@integer/fade_in_start_frame" android:alpha="0" /> <KeyPosition - app:keyPositionType="pathRelative" - app:percentX="0" - app:percentY="0" - app:framePosition="0" - app:curveFit="linear" - app:motionTarget="@id/statusIcons" /> - <KeyPosition - app:keyPositionType="pathRelative" + app:keyPositionType="deltaRelative" app:percentX="0" - app:percentY="0" - app:framePosition="50" + app:percentY="@dimen/percent_displacement_at_fade_out" + app:framePosition="@integer/fade_out_complete_frame" app:sizePercent="0" app:curveFit="linear" app:motionTarget="@id/statusIcons" /> <KeyPosition app:keyPositionType="deltaRelative" app:percentX="1" - app:percentY="0.51" - app:framePosition="51" + app:percentY="0.5" + app:framePosition="50" app:sizePercent="1" app:curveFit="linear" app:motionTarget="@id/statusIcons" /> <KeyAttribute app:motionTarget="@id/statusIcons" - app:framePosition="30" + app:framePosition="@integer/fade_out_complete_frame" android:alpha="0" /> <KeyAttribute app:motionTarget="@id/statusIcons" - app:framePosition="70" + app:framePosition="@integer/fade_in_start_frame" android:alpha="0" /> <KeyPosition app:keyPositionType="deltaRelative" app:percentX="0" - app:percentY="0" - app:framePosition="50" + app:percentY="@dimen/percent_displacement_at_fade_out" + app:framePosition="@integer/fade_out_complete_frame" app:percentWidth="1" app:percentHeight="1" app:curveFit="linear" @@ -95,27 +88,27 @@ <KeyPosition app:keyPositionType="deltaRelative" app:percentX="1" - app:percentY="0.51" - app:framePosition="51" + app:percentY="0.5" + app:framePosition="50" app:percentWidth="1" app:percentHeight="1" app:curveFit="linear" app:motionTarget="@id/batteryRemainingIcon" /> <KeyAttribute app:motionTarget="@id/batteryRemainingIcon" - app:framePosition="30" + app:framePosition="@integer/fade_out_complete_frame" android:alpha="0" /> <KeyAttribute app:motionTarget="@id/batteryRemainingIcon" - app:framePosition="70" + app:framePosition="@integer/fade_in_start_frame" android:alpha="0" /> <KeyPosition app:motionTarget="@id/carrier_group" app:percentX="1" - app:percentY="0.51" - app:framePosition="51" + app:percentY="0.5" + app:framePosition="50" app:percentWidth="1" app:percentHeight="1" app:curveFit="linear" @@ -126,7 +119,7 @@ android:alpha="0" /> <KeyAttribute app:motionTarget="@id/carrier_group" - app:framePosition="70" + app:framePosition="@integer/fade_in_start_frame" android:alpha="0" /> </KeyFrameSet> </Transition> diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml index a82684d0358b..88b4f43b440b 100644 --- a/packages/SystemUI/res/xml/qqs_header.xml +++ b/packages/SystemUI/res/xml/qqs_header.xml @@ -43,7 +43,8 @@ android:id="@+id/date"> <Layout android:layout_width="0dp" - android:layout_height="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" + android:layout_marginStart="8dp" app:layout_constrainedWidth="true" app:layout_constraintStart_toEndOf="@id/clock" app:layout_constraintEnd_toStartOf="@id/barrier" @@ -57,8 +58,8 @@ android:id="@+id/statusIcons"> <Layout android:layout_width="0dp" - android:layout_height="@dimen/qs_header_non_clickable_element_height" - app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" + app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height" app:layout_constraintStart_toEndOf="@id/date" app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" app:layout_constraintTop_toTopOf="parent" @@ -71,9 +72,9 @@ android:id="@+id/batteryRemainingIcon"> <Layout android:layout_width="wrap_content" - android:layout_height="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" app:layout_constrainedWidth="true" - app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height" + app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height" app:layout_constraintStart_toEndOf="@id/statusIcons" app:layout_constraintEnd_toEndOf="@id/end_guide" app:layout_constraintTop_toTopOf="parent" diff --git a/packages/SystemUI/res/xml/qs_header_new.xml b/packages/SystemUI/res/xml/qs_header_new.xml index f39e6bd65b86..d8a4e7752960 100644 --- a/packages/SystemUI/res/xml/qs_header_new.xml +++ b/packages/SystemUI/res/xml/qs_header_new.xml @@ -40,13 +40,13 @@ android:layout_height="@dimen/large_screen_shade_header_min_height" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/privacy_container" - app:layout_constraintBottom_toTopOf="@id/date" + app:layout_constraintBottom_toBottomOf="@id/carrier_group" app:layout_constraintEnd_toStartOf="@id/carrier_group" app:layout_constraintHorizontal_bias="0" /> <Transform - android:scaleX="2.4" - android:scaleY="2.4" + android:scaleX="2.57" + android:scaleY="2.57" /> </Constraint> @@ -54,11 +54,11 @@ android:id="@+id/date"> <Layout android:layout_width="0dp" - android:layout_height="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/space" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintTop_toBottomOf="@id/clock" + app:layout_constraintTop_toBottomOf="@id/carrier_group" app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_chainStyle="spread_inside" /> @@ -87,7 +87,7 @@ android:id="@+id/statusIcons"> <Layout android:layout_width="0dp" - android:layout_height="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" app:layout_constrainedWidth="true" app:layout_constraintStart_toEndOf="@id/space" app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" @@ -101,8 +101,8 @@ android:id="@+id/batteryRemainingIcon"> <Layout android:layout_width="wrap_content" - android:layout_height="@dimen/qs_header_non_clickable_element_height" - app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" + app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height" app:layout_constraintStart_toEndOf="@id/statusIcons" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/date" diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt index 2e391c7aacbe..49cc48321d77 100644 --- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt @@ -19,6 +19,7 @@ package com.android.systemui.testing.screenshot import android.app.Activity import android.graphics.Color import android.view.View +import android.view.Window import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat @@ -51,13 +52,14 @@ class ExternalViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestR /** * Compare the content of the [view] with the golden image identified by [goldenIdentifier] in - * the context of [emulationSpec]. + * the context of [emulationSpec]. Window must be specified to capture views that render + * hardware buffers. */ - fun screenshotTest(goldenIdentifier: String, view: View) { + fun screenshotTest(goldenIdentifier: String, view: View, window: Window? = null) { view.removeElevationRecursively() ScreenshotRuleAsserter.Builder(screenshotRule) - .setScreenshotProvider { view.toBitmap() } + .setScreenshotProvider { view.toBitmap(window) } .withMatcher(matcher) .build() .assertGoldenImage(goldenIdentifier) @@ -94,6 +96,6 @@ class ExternalViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestR activity.currentFocus?.clearFocus() } - screenshotTest(goldenIdentifier, rootView) + screenshotTest(goldenIdentifier, rootView, activity.window) } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index 134f3bc93847..1cf7c503a508 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -190,8 +190,13 @@ class AnimatableClockView @JvmOverloads constructor( override fun onDraw(canvas: Canvas) { lastDraw = getTimestamp() - // intentionally doesn't call super.onDraw here or else the text will be rendered twice - textAnimator?.draw(canvas) + // Use textAnimator to render text if animation is enabled. + // Otherwise default to using standard draw functions. + if (isAnimationEnabled) { + textAnimator?.draw(canvas) + } else { + super.onDraw(canvas) + } } override fun invalidate() { @@ -363,6 +368,9 @@ class AnimatableClockView @JvmOverloads constructor( onAnimationEnd = onAnimationEnd ) textAnimator?.glyphFilter = glyphFilter + if (color != null && !isAnimationEnabled) { + setTextColor(color) + } } else { // when the text animator is set, update its start values onTextAnimatorInitialized = Runnable { @@ -377,6 +385,9 @@ class AnimatableClockView @JvmOverloads constructor( onAnimationEnd = onAnimationEnd ) textAnimator?.glyphFilter = glyphFilter + if (color != null && !isAnimationEnabled) { + setTextColor(color) + } } } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt index f03fee4b0c2d..cd272635905b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -21,7 +21,7 @@ import android.os.Handler import android.os.UserHandle import android.provider.Settings import android.util.Log -import com.android.systemui.dagger.qualifiers.Main +import com.android.internal.annotations.Keep import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockId import com.android.systemui.plugins.ClockMetadata @@ -30,7 +30,6 @@ import com.android.systemui.plugins.ClockProviderPlugin import com.android.systemui.plugins.PluginListener import com.android.systemui.shared.plugins.PluginManager import com.google.gson.Gson -import javax.inject.Inject private val TAG = ClockRegistry::class.simpleName private const val DEBUG = true @@ -42,13 +41,6 @@ open class ClockRegistry( val handler: Handler, defaultClockProvider: ClockProvider ) { - @Inject constructor( - context: Context, - pluginManager: PluginManager, - @Main handler: Handler, - defaultClockProvider: DefaultClockProvider - ) : this(context, pluginManager, handler, defaultClockProvider as ClockProvider) { } - // Usually this would be a typealias, but a SAM provides better java interop fun interface ClockChangeListener { fun onClockChanged() @@ -201,6 +193,7 @@ open class ClockRegistry( val provider: ClockProvider ) + @Keep private data class ClockSetting( val clockId: ClockId, val _applied_timestamp: Long? diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java index 72f8b7b09dca..40c8774d4f34 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java @@ -1,13 +1,16 @@ package com.android.systemui.shared.recents.utilities; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; -import android.view.Surface; import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.wm.shell.util.SplitBounds; /** * Utility class to position the thumbnail in the TaskView @@ -16,10 +19,26 @@ public class PreviewPositionHelper { public static final float MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT = 0.1f; + /** + * Specifies that a stage is positioned at the top half of the screen if + * in portrait mode or at the left half of the screen if in landscape mode. + * TODO(b/254378592): Remove after consolidation + */ + public static final int STAGE_POSITION_TOP_OR_LEFT = 0; + + /** + * Specifies that a stage is positioned at the bottom half of the screen if + * in portrait mode or at the right half of the screen if in landscape mode. + * TODO(b/254378592): Remove after consolidation + */ + public static final int STAGE_POSITION_BOTTOM_OR_RIGHT = 1; + // Contains the portion of the thumbnail that is unclipped when fullscreen progress = 1. private final RectF mClippedInsets = new RectF(); private final Matrix mMatrix = new Matrix(); private boolean mIsOrientationChanged; + private SplitBounds mSplitBounds; + private int mDesiredStagePosition; public Matrix getMatrix() { return mMatrix; @@ -33,6 +52,11 @@ public class PreviewPositionHelper { return mIsOrientationChanged; } + public void setSplitBounds(SplitBounds splitBounds, int desiredStagePosition) { + mSplitBounds = splitBounds; + mDesiredStagePosition = desiredStagePosition; + } + /** * Updates the matrix based on the provided parameters */ @@ -42,10 +66,19 @@ public class PreviewPositionHelper { boolean isRotated = false; boolean isOrientationDifferent; + float fullscreenTaskWidth = screenWidthPx; + if (mSplitBounds != null && !mSplitBounds.appsStackedVertically) { + // For landscape, scale the width + float taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT + ? mSplitBounds.leftTaskPercent + : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent)); + // Scale landscape width to that of actual screen + fullscreenTaskWidth = screenWidthPx * taskPercent; + } int thumbnailRotation = thumbnailData.rotation; int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation); RectF thumbnailClipHint = new RectF(); - float canvasScreenRatio = canvasWidth / (float) screenWidthPx; + float canvasScreenRatio = canvasWidth / fullscreenTaskWidth; float scaledTaskbarSize = taskbarSize * canvasScreenRatio; thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0; @@ -180,7 +213,7 @@ public class PreviewPositionHelper { * portrait or vice versa, {@code false} otherwise */ private boolean isOrientationChange(int deltaRotation) { - return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270; + return deltaRotation == ROTATION_90 || deltaRotation == ROTATION_270; } private void setThumbnailRotation(int deltaRotate, Rect thumbnailPosition) { @@ -189,13 +222,13 @@ public class PreviewPositionHelper { mMatrix.setRotate(90 * deltaRotate); switch (deltaRotate) { /* Counter-clockwise */ - case Surface.ROTATION_90: + case ROTATION_90: translateX = thumbnailPosition.height(); break; - case Surface.ROTATION_270: + case ROTATION_270: translateY = thumbnailPosition.width(); break; - case Surface.ROTATION_180: + case ROTATION_180: translateX = thumbnailPosition.width(); translateY = thumbnailPosition.height(); break; diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt index dd2e55d4e7d7..cd4b9994ccca 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.shared.regionsampling +import android.graphics.Color import android.graphics.Rect import android.view.View import androidx.annotation.VisibleForTesting @@ -33,18 +34,19 @@ open class RegionSamplingInstance( regionSamplingEnabled: Boolean, updateFun: UpdateColorCallback ) { - private var isDark = RegionDarkness.DEFAULT + private var regionDarkness = RegionDarkness.DEFAULT private var samplingBounds = Rect() private val tmpScreenLocation = IntArray(2) @VisibleForTesting var regionSampler: RegionSamplingHelper? = null - + private var lightForegroundColor = Color.WHITE + private var darkForegroundColor = Color.BLACK /** * Interface for method to be passed into RegionSamplingHelper */ @FunctionalInterface interface UpdateColorCallback { /** - * Method to update the text colors after clock darkness changed. + * Method to update the foreground colors after clock darkness changed. */ fun updateColors() } @@ -59,6 +61,30 @@ open class RegionSamplingInstance( return RegionSamplingHelper(sampledView, callback, mainExecutor, bgExecutor) } + /** + * Sets the colors to be used for Dark and Light Foreground. + * + * @param lightColor The color used for Light Foreground. + * @param darkColor The color used for Dark Foreground. + */ + fun setForegroundColors(lightColor: Int, darkColor: Int) { + lightForegroundColor = lightColor + darkForegroundColor = darkColor + } + + /** + * Determines which foreground color to use based on region darkness. + * + * @return the determined foreground color + */ + fun currentForegroundColor(): Int{ + return if (regionDarkness.isDark) { + lightForegroundColor + } else { + darkForegroundColor + } + } + private fun convertToClockDarkness(isRegionDark: Boolean): RegionDarkness { return if (isRegionDark) { RegionDarkness.DARK @@ -68,7 +94,7 @@ open class RegionSamplingInstance( } fun currentRegionDarkness(): RegionDarkness { - return isDark + return regionDarkness } /** @@ -97,7 +123,7 @@ open class RegionSamplingInstance( regionSampler = createRegionSamplingHelper(sampledView, object : SamplingCallback { override fun onRegionDarknessChanged(isRegionDark: Boolean) { - isDark = convertToClockDarkness(isRegionDark) + regionDarkness = convertToClockDarkness(isRegionDark) updateFun.updateColors() } /** diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java index 5d6598d63a1b..8a2509610310 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java @@ -51,6 +51,8 @@ public final class InteractionJankMonitorWrapper { InteractionJankMonitor.CUJ_SPLIT_SCREEN_ENTER; public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION = InteractionJankMonitor.CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION; + public static final int CUJ_RECENTS_SCROLLING = + InteractionJankMonitor.CUJ_RECENTS_SCROLLING; @IntDef({ CUJ_APP_LAUNCH_FROM_RECENTS, @@ -59,7 +61,8 @@ public final class InteractionJankMonitorWrapper { CUJ_APP_CLOSE_TO_PIP, CUJ_QUICK_SWITCH, CUJ_APP_LAUNCH_FROM_WIDGET, - CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION + CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION, + CUJ_RECENTS_SCROLLING }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java deleted file mode 100644 index 30c062b66da9..000000000000 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Copyright (C) 2018 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.shared.system; - -import android.graphics.HardwareRenderer; -import android.graphics.Matrix; -import android.graphics.Rect; -import android.os.Handler; -import android.os.Handler.Callback; -import android.os.Message; -import android.os.Trace; -import android.view.SurfaceControl; -import android.view.SurfaceControl.Transaction; -import android.view.View; -import android.view.ViewRootImpl; - -import java.util.function.Consumer; - -/** - * Helper class to apply surface transactions in sync with RenderThread. - * - * NOTE: This is a modification of {@link android.view.SyncRtSurfaceTransactionApplier}, we can't - * currently reference that class from the shared lib as it is hidden. - */ -public class SyncRtSurfaceTransactionApplierCompat { - - public static final int FLAG_ALL = 0xffffffff; - public static final int FLAG_ALPHA = 1; - public static final int FLAG_MATRIX = 1 << 1; - public static final int FLAG_WINDOW_CROP = 1 << 2; - public static final int FLAG_LAYER = 1 << 3; - public static final int FLAG_CORNER_RADIUS = 1 << 4; - public static final int FLAG_BACKGROUND_BLUR_RADIUS = 1 << 5; - public static final int FLAG_VISIBILITY = 1 << 6; - public static final int FLAG_RELATIVE_LAYER = 1 << 7; - public static final int FLAG_SHADOW_RADIUS = 1 << 8; - - private static final int MSG_UPDATE_SEQUENCE_NUMBER = 0; - - private final SurfaceControl mBarrierSurfaceControl; - private final ViewRootImpl mTargetViewRootImpl; - private final Handler mApplyHandler; - - private int mSequenceNumber = 0; - private int mPendingSequenceNumber = 0; - private Runnable mAfterApplyCallback; - - /** - * @param targetView The view in the surface that acts as synchronization anchor. - */ - public SyncRtSurfaceTransactionApplierCompat(View targetView) { - mTargetViewRootImpl = targetView != null ? targetView.getViewRootImpl() : null; - mBarrierSurfaceControl = mTargetViewRootImpl != null - ? mTargetViewRootImpl.getSurfaceControl() : null; - - mApplyHandler = new Handler(new Callback() { - @Override - public boolean handleMessage(Message msg) { - if (msg.what == MSG_UPDATE_SEQUENCE_NUMBER) { - onApplyMessage(msg.arg1); - return true; - } - return false; - } - }); - } - - private void onApplyMessage(int seqNo) { - mSequenceNumber = seqNo; - if (mSequenceNumber == mPendingSequenceNumber && mAfterApplyCallback != null) { - Runnable r = mAfterApplyCallback; - mAfterApplyCallback = null; - r.run(); - } - } - - /** - * Schedules applying surface parameters on the next frame. - * - * @param params The surface parameters to apply. DO NOT MODIFY the list after passing into - * this method to avoid synchronization issues. - */ - public void scheduleApply(final SyncRtSurfaceTransactionApplierCompat.SurfaceParams... params) { - if (mTargetViewRootImpl == null || mTargetViewRootImpl.getView() == null) { - return; - } - - mPendingSequenceNumber++; - final int toApplySeqNo = mPendingSequenceNumber; - mTargetViewRootImpl.registerRtFrameCallback(new HardwareRenderer.FrameDrawingCallback() { - @Override - public void onFrameDraw(long frame) { - if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) { - Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0) - .sendToTarget(); - return; - } - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Sync transaction frameNumber=" + frame); - Transaction t = new Transaction(); - for (int i = params.length - 1; i >= 0; i--) { - SyncRtSurfaceTransactionApplierCompat.SurfaceParams surfaceParams = - params[i]; - surfaceParams.applyTo(t); - } - if (mTargetViewRootImpl != null) { - mTargetViewRootImpl.mergeWithNextTransaction(t, frame); - } else { - t.apply(); - } - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0) - .sendToTarget(); - } - }); - - // Make sure a frame gets scheduled. - mTargetViewRootImpl.getView().invalidate(); - } - - /** - * Calls the runnable when any pending apply calls have completed - */ - public void addAfterApplyCallback(final Runnable afterApplyCallback) { - if (mSequenceNumber == mPendingSequenceNumber) { - afterApplyCallback.run(); - } else { - if (mAfterApplyCallback == null) { - mAfterApplyCallback = afterApplyCallback; - } else { - final Runnable oldCallback = mAfterApplyCallback; - mAfterApplyCallback = new Runnable() { - @Override - public void run() { - afterApplyCallback.run(); - oldCallback.run(); - } - }; - } - } - } - - public static void applyParams(TransactionCompat t, - SyncRtSurfaceTransactionApplierCompat.SurfaceParams params) { - params.applyTo(t.mTransaction); - } - - /** - * Creates an instance of SyncRtSurfaceTransactionApplier, deferring until the target view is - * attached if necessary. - */ - public static void create(final View targetView, - final Consumer<SyncRtSurfaceTransactionApplierCompat> callback) { - if (targetView == null) { - // No target view, no applier - callback.accept(null); - } else if (targetView.getViewRootImpl() != null) { - // Already attached, we're good to go - callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView)); - } else { - // Haven't been attached before we can get the view root - targetView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - targetView.removeOnAttachStateChangeListener(this); - callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView)); - } - - @Override - public void onViewDetachedFromWindow(View v) { - // Do nothing - } - }); - } - } - - public static class SurfaceParams { - public static class Builder { - final SurfaceControl surface; - int flags; - float alpha; - float cornerRadius; - int backgroundBlurRadius; - Matrix matrix; - Rect windowCrop; - int layer; - SurfaceControl relativeTo; - int relativeLayer; - boolean visible; - float shadowRadius; - - /** - * @param surface The surface to modify. - */ - public Builder(SurfaceControl surface) { - this.surface = surface; - } - - /** - * @param alpha The alpha value to apply to the surface. - * @return this Builder - */ - public Builder withAlpha(float alpha) { - this.alpha = alpha; - flags |= FLAG_ALPHA; - return this; - } - - /** - * @param matrix The matrix to apply to the surface. - * @return this Builder - */ - public Builder withMatrix(Matrix matrix) { - this.matrix = new Matrix(matrix); - flags |= FLAG_MATRIX; - return this; - } - - /** - * @param windowCrop The window crop to apply to the surface. - * @return this Builder - */ - public Builder withWindowCrop(Rect windowCrop) { - this.windowCrop = new Rect(windowCrop); - flags |= FLAG_WINDOW_CROP; - return this; - } - - /** - * @param layer The layer to assign the surface. - * @return this Builder - */ - public Builder withLayer(int layer) { - this.layer = layer; - flags |= FLAG_LAYER; - return this; - } - - /** - * @param relativeTo The surface that's set relative layer to. - * @param relativeLayer The relative layer. - * @return this Builder - */ - public Builder withRelativeLayerTo(SurfaceControl relativeTo, int relativeLayer) { - this.relativeTo = relativeTo; - this.relativeLayer = relativeLayer; - flags |= FLAG_RELATIVE_LAYER; - return this; - } - - /** - * @param radius the Radius for rounded corners to apply to the surface. - * @return this Builder - */ - public Builder withCornerRadius(float radius) { - this.cornerRadius = radius; - flags |= FLAG_CORNER_RADIUS; - return this; - } - - /** - * @param radius the Radius for the shadows to apply to the surface. - * @return this Builder - */ - public Builder withShadowRadius(float radius) { - this.shadowRadius = radius; - flags |= FLAG_SHADOW_RADIUS; - return this; - } - - /** - * @param radius the Radius for blur to apply to the background surfaces. - * @return this Builder - */ - public Builder withBackgroundBlur(int radius) { - this.backgroundBlurRadius = radius; - flags |= FLAG_BACKGROUND_BLUR_RADIUS; - return this; - } - - /** - * @param visible The visibility to apply to the surface. - * @return this Builder - */ - public Builder withVisibility(boolean visible) { - this.visible = visible; - flags |= FLAG_VISIBILITY; - return this; - } - - /** - * @return a new SurfaceParams instance - */ - public SurfaceParams build() { - return new SurfaceParams(surface, flags, alpha, matrix, windowCrop, layer, - relativeTo, relativeLayer, cornerRadius, backgroundBlurRadius, visible, - shadowRadius); - } - } - - private SurfaceParams(SurfaceControl surface, int flags, float alpha, Matrix matrix, - Rect windowCrop, int layer, SurfaceControl relativeTo, int relativeLayer, - float cornerRadius, int backgroundBlurRadius, boolean visible, float shadowRadius) { - this.flags = flags; - this.surface = surface; - this.alpha = alpha; - this.matrix = matrix; - this.windowCrop = windowCrop; - this.layer = layer; - this.relativeTo = relativeTo; - this.relativeLayer = relativeLayer; - this.cornerRadius = cornerRadius; - this.backgroundBlurRadius = backgroundBlurRadius; - this.visible = visible; - this.shadowRadius = shadowRadius; - } - - private final int flags; - private final float[] mTmpValues = new float[9]; - - public final SurfaceControl surface; - public final float alpha; - public final float cornerRadius; - public final int backgroundBlurRadius; - public final Matrix matrix; - public final Rect windowCrop; - public final int layer; - public final SurfaceControl relativeTo; - public final int relativeLayer; - public final boolean visible; - public final float shadowRadius; - - public void applyTo(SurfaceControl.Transaction t) { - if ((flags & FLAG_MATRIX) != 0) { - t.setMatrix(surface, matrix, mTmpValues); - } - if ((flags & FLAG_WINDOW_CROP) != 0) { - t.setWindowCrop(surface, windowCrop); - } - if ((flags & FLAG_ALPHA) != 0) { - t.setAlpha(surface, alpha); - } - if ((flags & FLAG_LAYER) != 0) { - t.setLayer(surface, layer); - } - if ((flags & FLAG_CORNER_RADIUS) != 0) { - t.setCornerRadius(surface, cornerRadius); - } - if ((flags & FLAG_BACKGROUND_BLUR_RADIUS) != 0) { - t.setBackgroundBlurRadius(surface, backgroundBlurRadius); - } - if ((flags & FLAG_VISIBILITY) != 0) { - if (visible) { - t.show(surface); - } else { - t.hide(surface); - } - } - if ((flags & FLAG_RELATIVE_LAYER) != 0) { - t.setRelativeLayer(surface, relativeTo, relativeLayer); - } - if ((flags & FLAG_SHADOW_RADIUS) != 0) { - t.setShadowRadius(surface, shadowRadius); - } - } - } -} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java deleted file mode 100644 index 43a882a5f6be..000000000000 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2018 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.shared.system; - -import android.graphics.Matrix; -import android.graphics.Rect; -import android.view.SurfaceControl; -import android.view.SurfaceControl.Transaction; - -public class TransactionCompat { - - final Transaction mTransaction; - - final float[] mTmpValues = new float[9]; - - public TransactionCompat() { - mTransaction = new Transaction(); - } - - public void apply() { - mTransaction.apply(); - } - - public TransactionCompat show(SurfaceControl surfaceControl) { - mTransaction.show(surfaceControl); - return this; - } - - public TransactionCompat hide(SurfaceControl surfaceControl) { - mTransaction.hide(surfaceControl); - return this; - } - - public TransactionCompat setPosition(SurfaceControl surfaceControl, float x, float y) { - mTransaction.setPosition(surfaceControl, x, y); - return this; - } - - public TransactionCompat setSize(SurfaceControl surfaceControl, int w, int h) { - mTransaction.setBufferSize(surfaceControl, w, h); - return this; - } - - public TransactionCompat setLayer(SurfaceControl surfaceControl, int z) { - mTransaction.setLayer(surfaceControl, z); - return this; - } - - public TransactionCompat setAlpha(SurfaceControl surfaceControl, float alpha) { - mTransaction.setAlpha(surfaceControl, alpha); - return this; - } - - public TransactionCompat setOpaque(SurfaceControl surfaceControl, boolean opaque) { - mTransaction.setOpaque(surfaceControl, opaque); - return this; - } - - public TransactionCompat setMatrix(SurfaceControl surfaceControl, float dsdx, float dtdx, - float dtdy, float dsdy) { - mTransaction.setMatrix(surfaceControl, dsdx, dtdx, dtdy, dsdy); - return this; - } - - public TransactionCompat setMatrix(SurfaceControl surfaceControl, Matrix matrix) { - mTransaction.setMatrix(surfaceControl, matrix, mTmpValues); - return this; - } - - public TransactionCompat setWindowCrop(SurfaceControl surfaceControl, Rect crop) { - mTransaction.setWindowCrop(surfaceControl, crop); - return this; - } - - public TransactionCompat setCornerRadius(SurfaceControl surfaceControl, float radius) { - mTransaction.setCornerRadius(surfaceControl, radius); - return this; - } - - public TransactionCompat setBackgroundBlurRadius(SurfaceControl surfaceControl, int radius) { - mTransaction.setBackgroundBlurRadius(surfaceControl, radius); - return this; - } - - public TransactionCompat setColor(SurfaceControl surfaceControl, float[] color) { - mTransaction.setColor(surfaceControl, color); - return this; - } - - public static void setRelativeLayer(Transaction t, SurfaceControl surfaceControl, - SurfaceControl relativeTo, int z) { - t.setRelativeLayer(surfaceControl, relativeTo, z); - } -} diff --git a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt index 0075ddd73cd3..5277e40492e4 100644 --- a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt +++ b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt @@ -16,19 +16,29 @@ package com.android.keyguard +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator import android.content.Context import android.content.res.ColorStateList import android.content.res.TypedArray import android.graphics.Color import android.util.AttributeSet +import android.view.View import com.android.settingslib.Utils +import com.android.systemui.animation.Interpolators /** Displays security messages for the keyguard bouncer. */ -class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) : +open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) : KeyguardMessageArea(context, attrs) { private val DEFAULT_COLOR = -1 private var mDefaultColorState: ColorStateList? = null private var mNextMessageColorState: ColorStateList? = ColorStateList.valueOf(DEFAULT_COLOR) + private val animatorSet = AnimatorSet() + private var textAboutToShow: CharSequence? = null + protected open val SHOW_DURATION_MILLIS = 150L + protected open val HIDE_DURATION_MILLIS = 200L override fun updateTextColor() { var colorState = mDefaultColorState @@ -58,4 +68,46 @@ class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) : mDefaultColorState = Utils.getColorAttr(context, android.R.attr.textColorPrimary) super.reloadColor() } + + override fun setMessage(msg: CharSequence?) { + if (msg == textAboutToShow || msg == text) { + return + } + textAboutToShow = msg + + if (animatorSet.isRunning) { + animatorSet.cancel() + textAboutToShow = null + } + + val hideAnimator = + ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f).apply { + duration = HIDE_DURATION_MILLIS + interpolator = Interpolators.STANDARD_ACCELERATE + } + + hideAnimator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + super@BouncerKeyguardMessageArea.setMessage(msg) + } + } + ) + val showAnimator = + ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f).apply { + duration = SHOW_DURATION_MILLIS + interpolator = Interpolators.STANDARD_DECELERATE + } + + showAnimator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + textAboutToShow = null + } + } + ) + + animatorSet.playSequentially(hideAnimator, showAnimator) + animatorSet.start() + } } diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 9151238499dc..910955a45f7b 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -23,12 +23,18 @@ import android.content.res.Resources import android.text.format.DateFormat import android.util.TypedValue import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags.REGION_SAMPLING +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.ClockController -import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shared.regionsampling.RegionSamplingInstance import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback @@ -38,13 +44,20 @@ import java.util.Locale import java.util.TimeZone import java.util.concurrent.Executor import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch /** * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController]. */ open class ClockEventController @Inject constructor( - private val statusBarStateController: StatusBarStateController, + private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val broadcastDispatcher: BroadcastDispatcher, private val batteryController: BatteryController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, @@ -53,7 +66,7 @@ open class ClockEventController @Inject constructor( private val context: Context, @Main private val mainExecutor: Executor, @Background private val bgExecutor: Executor, - private val featureFlags: FeatureFlags, + private val featureFlags: FeatureFlags ) { var clock: ClockController? = null set(value) { @@ -70,9 +83,9 @@ open class ClockEventController @Inject constructor( private var isCharging = false private var dozeAmount = 0f private var isKeyguardVisible = false - - private val regionSamplingEnabled = - featureFlags.isEnabled(com.android.systemui.flags.Flags.REGION_SAMPLING) + private var isRegistered = false + private var disposableHandle: DisposableHandle? = null + private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING) private fun updateColors() { if (regionSamplingEnabled && smallRegionSampler != null && largeRegionSampler != null) { @@ -165,15 +178,6 @@ open class ClockEventController @Inject constructor( } } - private val statusBarStateListener = object : StatusBarStateController.StateListener { - override fun onDozeAmountChanged(linear: Float, eased: Float) { - clock?.animations?.doze(linear) - - isDozing = linear > dozeAmount - dozeAmount = linear - } - } - private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() { override fun onKeyguardVisibilityChanged(visible: Boolean) { isKeyguardVisible = visible @@ -195,13 +199,11 @@ open class ClockEventController @Inject constructor( } } - init { - isDozing = statusBarStateController.isDozing - } - - fun registerListeners() { - dozeAmount = statusBarStateController.dozeAmount - isDozing = statusBarStateController.isDozing || dozeAmount != 0f + fun registerListeners(parent: View) { + if (isRegistered) { + return + } + isRegistered = true broadcastDispatcher.registerReceiver( localeBroadcastReceiver, @@ -210,17 +212,28 @@ open class ClockEventController @Inject constructor( configurationController.addCallback(configListener) batteryController.addCallback(batteryCallback) keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) - statusBarStateController.addCallback(statusBarStateListener) smallRegionSampler?.startRegionSampler() largeRegionSampler?.startRegionSampler() + disposableHandle = parent.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + listenForDozing(this) + listenForDozeAmount(this) + listenForDozeAmountTransition(this) + } + } } fun unregisterListeners() { + if (!isRegistered) { + return + } + isRegistered = false + + disposableHandle?.dispose() broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver) configurationController.removeCallback(configListener) batteryController.removeCallback(batteryCallback) keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) - statusBarStateController.removeCallback(statusBarStateListener) smallRegionSampler?.stopRegionSampler() largeRegionSampler?.stopRegionSampler() } @@ -235,8 +248,39 @@ open class ClockEventController @Inject constructor( largeRegionSampler?.dump(pw) } - companion object { - private val TAG = ClockEventController::class.simpleName - private const val FORMAT_NUMBER = 1234567890 + @VisibleForTesting + internal fun listenForDozeAmount(scope: CoroutineScope): Job { + return scope.launch { + keyguardInteractor.dozeAmount.collect { + dozeAmount = it + clock?.animations?.doze(dozeAmount) + } + } + } + + @VisibleForTesting + internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job { + return scope.launch { + keyguardTransitionInteractor.aodToLockscreenTransition.collect { + // Would eventually run this: + // dozeAmount = it.value + // clock?.animations?.doze(dozeAmount) + } + } + } + + @VisibleForTesting + internal fun listenForDozing(scope: CoroutineScope): Job { + return scope.launch { + combine ( + keyguardInteractor.dozeAmount, + keyguardInteractor.isDozing, + ) { localDozeAmount, localIsDozing -> + localDozeAmount > dozeAmount || localIsDozing + } + .collect { localIsDozing -> + isDozing = localIsDozing + } + } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 20d064b960d2..8eebe30222ae 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -165,7 +165,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS protected void onViewAttached() { mClockRegistry.registerClockChangeListener(mClockChangedListener); setClock(mClockRegistry.createCurrentClock()); - mClockEventController.registerListeners(); + mClockEventController.registerListeners(mView); mKeyguardClockTopMargin = mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index f26b9057dc7c..73229c321079 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -152,6 +152,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> } public void startAppearAnimation() { + mMessageAreaController.setMessage(getInitialMessageResId()); mView.startAppearAnimation(); } @@ -169,6 +170,11 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> return view.indexOfChild(mView); } + /** Determines the message to show in the bouncer when it first appears. */ + protected int getInitialMessageResId() { + return 0; + } + /** Factory for a {@link KeyguardInputViewController}. */ public static class Factory { private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java index c2802f7b6843..2bd3ca59b740 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java @@ -18,7 +18,6 @@ package com.android.keyguard; import android.content.res.ColorStateList; import android.content.res.Configuration; -import android.text.TextUtils; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; @@ -100,15 +99,6 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> mView.setMessage(resId); } - /** - * Set Text if KeyguardMessageArea is empty. - */ - public void setMessageIfEmpty(int resId) { - if (TextUtils.isEmpty(mView.getText())) { - setMessage(resId); - } - } - public void setNextMessageColor(ColorStateList colorState) { mView.setNextMessageColor(colorState); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java index 29e912fdab32..0025986c0e5c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java @@ -187,7 +187,7 @@ public class KeyguardPasswordViewController @Override void resetState() { mPasswordEntry.setTextOperationUser(UserHandle.of(KeyguardUpdateMonitor.getCurrentUser())); - mMessageAreaController.setMessage(""); + mMessageAreaController.setMessage(getInitialMessageResId()); final boolean wasDisabled = mPasswordEntry.isEnabled(); mView.setPasswordEntryEnabled(true); mView.setPasswordEntryInputEnabled(true); @@ -207,7 +207,6 @@ public class KeyguardPasswordViewController if (reason != KeyguardSecurityView.SCREEN_ON || mShowImeAtScreenOn) { showInput(); } - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_password); } private void showInput() { @@ -324,4 +323,9 @@ public class KeyguardPasswordViewController //enabled input method subtype (The current IME should be LatinIME.) || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1; } + + @Override + protected int getInitialMessageResId() { + return R.string.keyguard_enter_your_password; + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index 987164557a7a..1f0bd54f8e09 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -298,12 +298,6 @@ public class KeyguardPatternViewController } @Override - public void onResume(int reason) { - super.onResume(reason); - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pattern); - } - - @Override public boolean needsInput() { return false; } @@ -361,7 +355,7 @@ public class KeyguardPatternViewController } private void displayDefaultSecurityMessage() { - mMessageAreaController.setMessage(""); + mMessageAreaController.setMessage(getInitialMessageResId()); } private void handleAttemptLockout(long elapsedRealtimeDeadline) { @@ -392,4 +386,9 @@ public class KeyguardPatternViewController }.start(); } + + @Override + protected int getInitialMessageResId() { + return R.string.keyguard_enter_your_pattern; + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index 59a018ad51df..f7423ed12e68 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -127,7 +127,6 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB public void onResume(int reason) { super.onResume(reason); mPasswordEntry.requestFocus(); - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin); } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index 89fcc47caf57..7876f071fdf5 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -76,20 +76,13 @@ public class KeyguardPinViewController } @Override - void resetState() { - super.resetState(); - mMessageAreaController.setMessage(""); - } - - @Override - public void startAppearAnimation() { - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin); - super.startAppearAnimation(); - } - - @Override public boolean startDisappearAnimation(Runnable finishRunnable) { return mView.startDisappearAnimation( mKeyguardUpdateMonitor.needsSlowUnlockTransition(), finishRunnable); } + + @Override + protected int getInitialMessageResId() { + return R.string.keyguard_enter_your_pin; + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index f5582761c0ae..8699e324adf9 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -106,7 +106,6 @@ import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; -import android.os.ServiceManager; import android.os.SystemClock; import android.os.Trace; import android.os.UserHandle; @@ -3796,4 +3795,17 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } mListenModels.print(pw); } + + /** + * Schedules a watchdog for the face and fingerprint BiometricScheduler. + * Cancels all operations in the scheduler if it is hung for 10 seconds. + */ + public void startBiometricWatchdog() { + if (mFaceManager != null) { + mFaceManager.scheduleWatchdog(); + } + if (mFpm != null) { + mFpm.scheduleWatchdog(); + } + } } diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java index c4be1ba53503..72a44bd198f2 100644 --- a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java +++ b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java @@ -21,9 +21,14 @@ import java.util.List; import dagger.Module; import dagger.Provides; -/** Dagger Module for clock package. */ +/** + * Dagger Module for clock package. + * + * @deprecated Migrate to ClockRegistry + */ @Module -public abstract class ClockModule { +@Deprecated +public abstract class ClockInfoModule { /** */ @Provides diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java new file mode 100644 index 000000000000..f43f559b4234 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard.dagger; + +import android.content.Context; +import android.os.Handler; + +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.shared.clocks.ClockRegistry; +import com.android.systemui.shared.clocks.DefaultClockProvider; +import com.android.systemui.shared.plugins.PluginManager; + +import dagger.Module; +import dagger.Provides; + +/** Dagger Module for clocks. */ +@Module +public abstract class ClockRegistryModule { + /** Provide the ClockRegistry as a singleton so that it is not instantiated more than once. */ + @Provides + @SysUISingleton + public static ClockRegistry getClockRegistry( + @Application Context context, + PluginManager pluginManager, + @Main Handler handler, + DefaultClockProvider defaultClockProvider) { + return new ClockRegistry(context, pluginManager, handler, defaultClockProvider); + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt index 2c2ab7b39161..6264ce7273f1 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt @@ -17,9 +17,9 @@ package com.android.keyguard.logging import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG import com.android.systemui.log.dagger.BiometricMessagesLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG import javax.inject.Inject /** Helper class for logging for [com.android.systemui.biometrics.FaceHelpMessageDeferral] */ diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt index 50012a589b5a..46f3d4e5f6aa 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt @@ -16,15 +16,15 @@ package com.android.keyguard.logging -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.VERBOSE -import com.android.systemui.log.LogLevel.WARNING -import com.android.systemui.log.MessageInitializer -import com.android.systemui.log.MessagePrinter import com.android.systemui.log.dagger.KeyguardLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.VERBOSE +import com.android.systemui.plugins.log.LogLevel.WARNING +import com.android.systemui.plugins.log.MessageInitializer +import com.android.systemui.plugins.log.MessagePrinter import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 2eee95738b7b..82b32cf616ec 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -22,13 +22,13 @@ import android.telephony.SubscriptionInfo import com.android.keyguard.ActiveUnlockConfig import com.android.keyguard.KeyguardListenModel import com.android.keyguard.KeyguardUpdateMonitorCallback -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.VERBOSE -import com.android.systemui.log.LogLevel.WARNING +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.VERBOSE +import com.android.systemui.plugins.log.LogLevel.WARNING import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 242a5983a59d..9493975ca00f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -792,7 +792,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mUdfpsBounds = udfpsProp.getLocation().getRect(); mUdfpsBounds.scale(mScaleFactor); mUdfpsController.updateOverlayParams(udfpsProp.sensorId, - new UdfpsOverlayParams(mUdfpsBounds, mCachedDisplayInfo.getNaturalWidth(), + new UdfpsOverlayParams(mUdfpsBounds, new Rect( + 0, mCachedDisplayInfo.getNaturalHeight() / 2, + mCachedDisplayInfo.getNaturalWidth(), + mCachedDisplayInfo.getNaturalHeight()), + mCachedDisplayInfo.getNaturalWidth(), mCachedDisplayInfo.getNaturalHeight(), mScaleFactor, mCachedDisplayInfo.rotation)); if (!Objects.equals(previousUdfpsBounds, mUdfpsBounds)) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java index 5ed898682883..76cd3f4c4f1d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java @@ -24,6 +24,7 @@ import android.content.Context; import android.graphics.Insets; import android.os.UserHandle; import android.text.InputType; +import android.text.TextUtils; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.View; @@ -151,39 +152,52 @@ public class AuthCredentialPasswordView extends AuthCredentialView protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - if (mAuthCredentialInput == null || mAuthCredentialHeader == null - || mSubtitleView == null || mPasswordField == null || mErrorView == null) { + if (mAuthCredentialInput == null || mAuthCredentialHeader == null || mSubtitleView == null + || mDescriptionView == null || mPasswordField == null || mErrorView == null) { return; } - // b/157910732 In AuthContainerView#getLayoutParams() we used to prevent jank risk when - // resizing by IME show or hide, we used to setFitInsetsTypes `~WindowInsets.Type.ime()` to - // LP. As a result this view needs to listen onApplyWindowInsets() and handle onLayout. int inputLeftBound; int inputTopBound; int headerRightBound = right; + int headerTopBounds = top; + final int subTitleBottom = (mSubtitleView.getVisibility() == GONE) ? mTitleView.getBottom() + : mSubtitleView.getBottom(); + final int descBottom = (mDescriptionView.getVisibility() == GONE) ? subTitleBottom + : mDescriptionView.getBottom(); if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { - inputTopBound = (bottom - (mPasswordField.getHeight() + mErrorView.getHeight())) / 2; + inputTopBound = (bottom - mAuthCredentialInput.getHeight()) / 2; inputLeftBound = (right - left) / 2; headerRightBound = inputLeftBound; + headerTopBounds -= Math.min(mIconView.getBottom(), mBottomInset); } else { - inputTopBound = mSubtitleView.getBottom() + (bottom - mSubtitleView.getBottom()) / 2; + inputTopBound = + descBottom + (bottom - descBottom - mAuthCredentialInput.getHeight()) / 2; inputLeftBound = (right - left - mAuthCredentialInput.getWidth()) / 2; } - mAuthCredentialHeader.layout(left, top, headerRightBound, bottom); + if (mDescriptionView.getBottom() > mBottomInset) { + mAuthCredentialHeader.layout(left, headerTopBounds, headerRightBound, bottom); + } mAuthCredentialInput.layout(inputLeftBound, inputTopBound, right, bottom); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final int newWidth = MeasureSpec.getSize(widthMeasureSpec); final int newHeight = MeasureSpec.getSize(heightMeasureSpec) - mBottomInset; - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), newHeight); + setMeasuredDimension(newWidth, newHeight); - measureChildren(widthMeasureSpec, - MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.AT_MOST)); + final int halfWidthSpec = MeasureSpec.makeMeasureSpec(getWidth() / 2, + MeasureSpec.AT_MOST); + final int fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED); + if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { + measureChildren(halfWidthSpec, fullHeightSpec); + } else { + measureChildren(widthMeasureSpec, fullHeightSpec); + } } @NonNull @@ -193,6 +207,20 @@ public class AuthCredentialPasswordView extends AuthCredentialView final Insets bottomInset = insets.getInsets(ime()); if (v instanceof AuthCredentialPasswordView && mBottomInset != bottomInset.bottom) { mBottomInset = bottomInset.bottom; + if (mBottomInset > 0 + && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { + mTitleView.setSingleLine(true); + mTitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE); + mTitleView.setMarqueeRepeatLimit(-1); + // select to enable marquee unless a screen reader is enabled + mTitleView.setSelected(!mAccessibilityManager.isEnabled() + || !mAccessibilityManager.isTouchExplorationEnabled()); + } else { + mTitleView.setSingleLine(false); + mTitleView.setEllipsize(null); + // select to enable marquee unless a screen reader is enabled + mTitleView.setSelected(false); + } requestLayout(); } return insets; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java index 11498dbc0b83..f9e44a0c1724 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java @@ -93,7 +93,9 @@ public class AuthCredentialPatternView extends AuthCredentialView { @Override protected void onErrorTimeoutFinish() { super.onErrorTimeoutFinish(); - mLockPatternView.setEnabled(true); + // select to enable marquee unless a screen reader is enabled + mLockPatternView.setEnabled(!mAccessibilityManager.isEnabled() + || !mAccessibilityManager.isTouchExplorationEnabled()); } public AuthCredentialPatternView(Context context, AttributeSet attrs) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java index d4176acf10f9..fa623d146756 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java @@ -77,7 +77,7 @@ public abstract class AuthCredentialView extends LinearLayout { protected final Handler mHandler; protected final LockPatternUtils mLockPatternUtils; - private final AccessibilityManager mAccessibilityManager; + protected final AccessibilityManager mAccessibilityManager; private final UserManager mUserManager; private final DevicePolicyManager mDevicePolicyManager; @@ -86,10 +86,10 @@ public abstract class AuthCredentialView extends LinearLayout { private boolean mShouldAnimatePanel; private boolean mShouldAnimateContents; - private TextView mTitleView; + protected TextView mTitleView; protected TextView mSubtitleView; - private TextView mDescriptionView; - private ImageView mIconView; + protected TextView mDescriptionView; + protected ImageView mIconView; protected TextView mErrorView; protected @Utils.CredentialType int mCredentialType; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt index 39199d194cc9..0d08b4307f12 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt @@ -16,12 +16,12 @@ package com.android.systemui.biometrics -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.VERBOSE -import com.android.systemui.log.LogLevel.WARNING import com.android.systemui.log.dagger.UdfpsLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.VERBOSE +import com.android.systemui.plugins.log.LogLevel.WARNING import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt new file mode 100644 index 000000000000..6e78f3d3d6aa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.PixelFormat +import android.graphics.Rect +import android.hardware.fingerprint.FingerprintManager +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal +import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback +import android.os.Handler +import android.view.MotionEvent +import android.view.View +import android.view.WindowManager +import android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.concurrency.Execution +import java.util.* +import java.util.concurrent.Executor +import javax.inject.Inject + +private const val TAG = "UdfpsOverlay" + +@SuppressLint("ClickableViewAccessibility") +@SysUISingleton +class UdfpsOverlay +@Inject +constructor( + private val context: Context, + private val execution: Execution, + private val windowManager: WindowManager, + private val fingerprintManager: FingerprintManager?, + private val handler: Handler, + private val biometricExecutor: Executor, + private val alternateTouchProvider: Optional<AlternateUdfpsTouchProvider>, + private val fgExecutor: DelayableExecutor, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val authController: AuthController, + private val udfpsLogger: UdfpsLogger +) : CoreStartable { + + /** The view, when [isShowing], or null. */ + var overlayView: UdfpsOverlayView? = null + private set + + private var requestId: Long = 0 + private var onFingerDown = false + val size = windowManager.maximumWindowMetrics.bounds + val udfpsProps: MutableList<FingerprintSensorPropertiesInternal> = mutableListOf() + + private var params: UdfpsOverlayParams = UdfpsOverlayParams() + + private val coreLayoutParams = + WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG, + 0 /* flags set in computeLayoutParams() */, + PixelFormat.TRANSLUCENT + ) + .apply { + title = TAG + fitInsetsTypes = 0 + gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT + layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + flags = Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS + privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY + // Avoid announcing window title. + accessibilityTitle = " " + inputFeatures = INPUT_FEATURE_SPY + } + + fun onTouch(v: View, event: MotionEvent): Boolean { + val view = v as UdfpsOverlayView + + return when (event.action) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_MOVE -> { + onFingerDown = true + if (!view.isDisplayConfigured && alternateTouchProvider.isPresent) { + biometricExecutor.execute { + alternateTouchProvider + .get() + .onPointerDown( + requestId, + event.x.toInt(), + event.y.toInt(), + event.touchMinor, + event.touchMajor + ) + } + fgExecutor.execute { + if (keyguardUpdateMonitor.isFingerprintDetectionRunning) { + keyguardUpdateMonitor.onUdfpsPointerDown(requestId.toInt()) + } + } + + view.configureDisplay { + biometricExecutor.execute { alternateTouchProvider.get().onUiReady() } + } + } + + true + } + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> { + if (onFingerDown && alternateTouchProvider.isPresent) { + biometricExecutor.execute { + alternateTouchProvider.get().onPointerUp(requestId) + } + fgExecutor.execute { + if (keyguardUpdateMonitor.isFingerprintDetectionRunning) { + keyguardUpdateMonitor.onUdfpsPointerUp(requestId.toInt()) + } + } + } + onFingerDown = false + if (view.isDisplayConfigured) { + view.unconfigureDisplay() + } + + true + } + else -> false + } + } + + fun show(requestId: Long): Boolean { + this.requestId = requestId + if (overlayView == null && alternateTouchProvider.isPresent) { + UdfpsOverlayView(context, null).let { + it.overlayParams = params + it.setUdfpsDisplayMode( + UdfpsDisplayMode(context, execution, authController, udfpsLogger) + ) + it.setOnTouchListener { v, event -> onTouch(v, event) } + overlayView = it + } + windowManager.addView(overlayView, coreLayoutParams) + return true + } + + return false + } + + fun hide() { + overlayView?.apply { + windowManager.removeView(this) + setOnTouchListener(null) + } + + overlayView = null + } + + @Override + override fun start() { + fingerprintManager?.addAuthenticatorsRegisteredCallback( + object : IFingerprintAuthenticatorsRegisteredCallback.Stub() { + override fun onAllAuthenticatorsRegistered( + sensors: List<FingerprintSensorPropertiesInternal> + ) { + handler.post { handleAllFingerprintAuthenticatorsRegistered(sensors) } + } + } + ) + } + + private fun handleAllFingerprintAuthenticatorsRegistered( + sensors: List<FingerprintSensorPropertiesInternal> + ) { + for (props in sensors) { + if (props.isAnyUdfpsType) { + udfpsProps.add(props) + } + } + + // Setup param size + if (udfpsProps.isNotEmpty()) { + params = + UdfpsOverlayParams( + sensorBounds = udfpsProps[0].location.rect, + overlayBounds = Rect(0, size.height() / 2, size.width(), size.height()), + naturalDisplayWidth = size.width(), + naturalDisplayHeight = size.height(), + scaleFactor = 1f + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt index d725dfbfe216..c23b0f09f099 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt @@ -20,6 +20,7 @@ import android.view.Surface.Rotation data class UdfpsOverlayParams( val sensorBounds: Rect = Rect(), + val overlayBounds: Rect = Rect(), val naturalDisplayWidth: Int = 0, val naturalDisplayHeight: Int = 0, val scaleFactor: Float = 1f, @@ -40,4 +41,4 @@ data class UdfpsOverlayParams( } else { naturalDisplayHeight } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt new file mode 100644 index 000000000000..d37133239531 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.widget.FrameLayout + +private const val TAG = "UdfpsOverlayView" + +class UdfpsOverlayView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { + + private val sensorRect = RectF() + var overlayParams = UdfpsOverlayParams() + private var mUdfpsDisplayMode: UdfpsDisplayMode? = null + + var overlayPaint = Paint() + var sensorPaint = Paint() + val centerPaint = Paint() + + /** True after the call to [configureDisplay] and before the call to [unconfigureDisplay]. */ + var isDisplayConfigured: Boolean = false + private set + + init { + this.setWillNotDraw(false) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + overlayPaint.color = Color.argb(120, 255, 0, 0) + overlayPaint.style = Paint.Style.FILL + + sensorPaint.color = Color.argb(150, 134, 204, 255) + sensorPaint.style = Paint.Style.FILL + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + canvas.drawRect(overlayParams.overlayBounds, overlayPaint) + canvas.drawRect(overlayParams.sensorBounds, sensorPaint) + canvas.drawCircle( + overlayParams.sensorBounds.exactCenterX(), + overlayParams.sensorBounds.exactCenterY(), + overlayParams.sensorBounds.width().toFloat() / 2, + centerPaint + ) + } + + fun setUdfpsDisplayMode(udfpsDisplayMode: UdfpsDisplayMode?) { + mUdfpsDisplayMode = udfpsDisplayMode + } + + fun configureDisplay(onDisplayConfigured: Runnable) { + isDisplayConfigured = true + mUdfpsDisplayMode?.enable(onDisplayConfigured) + } + + fun unconfigureDisplay() { + isDisplayConfigured = false + mUdfpsDisplayMode?.disable(null /* onDisabled */) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt index b1d6e003319a..75640b787a62 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics +import android.content.Context import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER @@ -23,9 +24,9 @@ import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTING import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR import android.hardware.biometrics.BiometricOverlayConstants.REASON_UNKNOWN -import android.hardware.fingerprint.IUdfpsOverlayController import android.hardware.fingerprint.IUdfpsOverlayControllerCallback import android.util.Log +import android.view.LayoutInflater import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry @@ -41,14 +42,17 @@ private const val SENSOR_ID = 0 */ @SysUISingleton class UdfpsShell @Inject constructor( - commandRegistry: CommandRegistry + commandRegistry: CommandRegistry, + private val udfpsOverlay: UdfpsOverlay ) : Command { /** * Set in [UdfpsController.java] constructor, used to show and hide the UDFPS overlay. * TODO: inject after b/229290039 is resolved */ - var udfpsOverlayController: IUdfpsOverlayController? = null + var udfpsOverlayController: UdfpsController.UdfpsOverlayController? = null + var context: Context? = null + var inflater: LayoutInflater? = null init { commandRegistry.registerCommand("udfps") { this } @@ -57,6 +61,11 @@ class UdfpsShell @Inject constructor( override fun execute(pw: PrintWriter, args: List<String>) { if (args.size == 1 && args[0] == "hide") { hideOverlay() + } else if (args.size == 2 && args[0] == "udfpsOverlay" && args[1] == "show") { + hideOverlay() + showUdfpsOverlay() + } else if (args.size == 2 && args[0] == "udfpsOverlay" && args[1] == "hide") { + hideUdfpsOverlay() } else if (args.size == 2 && args[0] == "show") { showOverlay(getEnrollmentReason(args[1])) } else { @@ -104,7 +113,17 @@ class UdfpsShell @Inject constructor( ) } + private fun showUdfpsOverlay() { + Log.v(TAG, "showUdfpsOverlay") + udfpsOverlay.show(REQUEST_ID) + } + + private fun hideUdfpsOverlay() { + Log.v(TAG, "hideUdfpsOverlay") + udfpsOverlay.hide() + } + private fun hideOverlay() { udfpsOverlayController?.hideUdfpsOverlay(SENSOR_ID) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt index 96af42bfda22..d99625a9fbf2 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt @@ -17,9 +17,9 @@ package com.android.systemui.bluetooth import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.BluetoothLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** Helper class for logging bluetooth events. */ diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt index 5b3a982ab5e2..d27708fc04d7 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt @@ -20,11 +20,11 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogMessage +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogMessage import com.android.systemui.log.dagger.BroadcastDispatcherLog import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt index bebade0cc484..08e8293cbe9c 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt @@ -17,6 +17,7 @@ package com.android.systemui.common.shared.model import android.annotation.StringRes +import android.content.Context /** * Models a content description, that can either be already [loaded][ContentDescription.Loaded] or @@ -30,4 +31,20 @@ sealed class ContentDescription { data class Resource( @StringRes val res: Int, ) : ContentDescription() + + companion object { + /** + * Returns the loaded content description string, or null if we don't have one. + * + * Prefer [com.android.systemui.common.ui.binder.ContentDescriptionViewBinder.bind] over + * this method. This should only be used for testing or concatenation purposes. + */ + fun ContentDescription?.loadContentDescription(context: Context): String? { + return when (this) { + null -> null + is Loaded -> this.description + is Resource -> context.getString(this.res) + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt index 5d0e08ffc307..4a5693202dba 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt @@ -18,6 +18,7 @@ package com.android.systemui.common.shared.model import android.annotation.StringRes +import android.content.Context /** * Models a text, that can either be already [loaded][Text.Loaded] or be a [reference] @@ -31,4 +32,20 @@ sealed class Text { data class Resource( @StringRes val res: Int, ) : Text() + + companion object { + /** + * Returns the loaded test string, or null if we don't have one. + * + * Prefer [com.android.systemui.common.ui.binder.TextViewBinder.bind] over this method. This + * should only be used for testing or concatenation purposes. + */ + fun Text?.loadText(context: Context): String? { + return when (this) { + null -> null + is Loaded -> this.text + is Resource -> context.getString(this.res) + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt index 77b65233c112..d3b5d0edd222 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt @@ -21,6 +21,8 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Bundle +import android.os.RemoteException +import android.service.dreams.IDreamManager import android.view.View import android.view.ViewGroup import android.view.WindowInsets @@ -40,11 +42,13 @@ import javax.inject.Inject */ class ControlsActivity @Inject constructor( private val uiController: ControlsUiController, - private val broadcastDispatcher: BroadcastDispatcher + private val broadcastDispatcher: BroadcastDispatcher, + private val dreamManager: IDreamManager, ) : ComponentActivity() { private lateinit var parent: ViewGroup private lateinit var broadcastReceiver: BroadcastReceiver + private var mExitToDream: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -81,17 +85,36 @@ class ControlsActivity @Inject constructor( parent = requireViewById<ViewGroup>(R.id.global_actions_controls) parent.alpha = 0f - uiController.show(parent, { finish() }, this) + uiController.show(parent, { finishOrReturnToDream() }, this) ControlsAnimations.enterAnimation(parent).start() } - override fun onBackPressed() { + override fun onResume() { + super.onResume() + mExitToDream = intent.getBooleanExtra(ControlsUiController.EXIT_TO_DREAM, false) + } + + fun finishOrReturnToDream() { + if (mExitToDream) { + try { + mExitToDream = false + dreamManager.dream() + return + } catch (e: RemoteException) { + // Fall through + } + } finish() } + override fun onBackPressed() { + finishOrReturnToDream() + } + override fun onStop() { super.onStop() + mExitToDream = false uiController.hide() } @@ -106,7 +129,8 @@ class ControlsActivity @Inject constructor( broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val action = intent.getAction() - if (Intent.ACTION_SCREEN_OFF.equals(action)) { + if (action == Intent.ACTION_SCREEN_OFF || + action == Intent.ACTION_DREAMING_STARTED) { finish() } } @@ -114,6 +138,7 @@ class ControlsActivity @Inject constructor( val filter = IntentFilter() filter.addAction(Intent.ACTION_SCREEN_OFF) + filter.addAction(Intent.ACTION_DREAMING_STARTED) broadcastDispatcher.registerReceiver(broadcastReceiver, filter) } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt index 822f8f2e6191..c1cfbcb0c211 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt @@ -27,6 +27,7 @@ interface ControlsUiController { companion object { public const val TAG = "ControlsUiController" public const val EXTRA_ANIMATE = "extra_animate" + public const val EXIT_TO_DREAM = "extra_exit_to_dream" } fun show(parent: ViewGroup, onDismiss: Runnable, activityContext: Context) diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 721c0ba4f865..09743ef7aebf 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -25,6 +25,7 @@ import com.android.systemui.SliceBroadcastRelayHandler import com.android.systemui.accessibility.SystemActions import com.android.systemui.accessibility.WindowMagnification import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.UdfpsOverlay import com.android.systemui.clipboardoverlay.ClipboardListener import com.android.systemui.dagger.qualifiers.PerUser import com.android.systemui.globalactions.GlobalActionsComponent @@ -218,6 +219,12 @@ abstract class SystemUICoreStartableModule { @ClassKey(KeyguardLiftController::class) abstract fun bindKeyguardLiftController(sysui: KeyguardLiftController): CoreStartable + /** Inject into UdfpsOverlay. */ + @Binds + @IntoMap + @ClassKey(UdfpsOverlay::class) + abstract fun bindUdfpsOverlay(sysui: UdfpsOverlay): CoreStartable + /** Inject into MediaTttSenderCoordinator. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index d7638d663dc9..7e31626983e7 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -23,7 +23,8 @@ import android.service.dreams.IDreamManager; import androidx.annotation.Nullable; import com.android.internal.statusbar.IStatusBarService; -import com.android.keyguard.clock.ClockModule; +import com.android.keyguard.clock.ClockInfoModule; +import com.android.keyguard.dagger.ClockRegistryModule; import com.android.keyguard.dagger.KeyguardBouncerComponent; import com.android.systemui.BootCompleteCache; import com.android.systemui.BootCompleteCacheImpl; @@ -120,7 +121,8 @@ import dagger.Provides; BiometricsModule.class, BouncerViewModule.class, ClipboardOverlayModule.class, - ClockModule.class, + ClockInfoModule.class, + ClockRegistryModule.class, CoroutinesModule.class, DreamModule.class, ControlsModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt index cc5766210406..0e1bfba8aadb 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt @@ -19,10 +19,10 @@ package com.android.systemui.doze import android.view.Display import com.android.systemui.doze.DozeLog.Reason import com.android.systemui.doze.DozeLog.reasonToString -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.log.dagger.DozeLog import com.android.systemui.statusbar.policy.DevicePostureController import java.text.SimpleDateFormat diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java index ae412152b10e..96c35d43052e 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java @@ -20,7 +20,6 @@ import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWA import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING; import android.annotation.MainThread; -import android.app.UiModeManager; import android.content.res.Configuration; import android.hardware.display.AmbientDisplayConfiguration; import android.os.Trace; @@ -145,10 +144,9 @@ public class DozeMachine { private final Service mDozeService; private final WakeLock mWakeLock; - private final AmbientDisplayConfiguration mConfig; + private final AmbientDisplayConfiguration mAmbientDisplayConfig; private final WakefulnessLifecycle mWakefulnessLifecycle; private final DozeHost mDozeHost; - private final UiModeManager mUiModeManager; private final DockManager mDockManager; private final Part[] mParts; @@ -156,18 +154,18 @@ public class DozeMachine { private State mState = State.UNINITIALIZED; private int mPulseReason; private boolean mWakeLockHeldForCurrentState = false; + private int mUiModeType = Configuration.UI_MODE_TYPE_NORMAL; @Inject - public DozeMachine(@WrappedService Service service, AmbientDisplayConfiguration config, + public DozeMachine(@WrappedService Service service, + AmbientDisplayConfiguration ambientDisplayConfig, WakeLock wakeLock, WakefulnessLifecycle wakefulnessLifecycle, - UiModeManager uiModeManager, DozeLog dozeLog, DockManager dockManager, DozeHost dozeHost, Part[] parts) { mDozeService = service; - mConfig = config; + mAmbientDisplayConfig = ambientDisplayConfig; mWakefulnessLifecycle = wakefulnessLifecycle; mWakeLock = wakeLock; - mUiModeManager = uiModeManager; mDozeLog = dozeLog; mDockManager = dockManager; mDozeHost = dozeHost; @@ -187,6 +185,18 @@ public class DozeMachine { } /** + * Notifies the {@link DozeMachine} that {@link Configuration} has changed. + */ + public void onConfigurationChanged(Configuration newConfiguration) { + int newUiModeType = newConfiguration.uiMode & Configuration.UI_MODE_TYPE_MASK; + if (mUiModeType == newUiModeType) return; + mUiModeType = newUiModeType; + for (Part part : mParts) { + part.onUiModeTypeChanged(mUiModeType); + } + } + + /** * Requests transitioning to {@code requestedState}. * * This can be called during a state transition, in which case it will be queued until all @@ -211,6 +221,14 @@ public class DozeMachine { requestState(State.DOZE_REQUEST_PULSE, pulseReason); } + /** + * @return true if {@link DozeMachine} is currently in either {@link State#UNINITIALIZED} + * or {@link State#FINISH} + */ + public boolean isUninitializedOrFinished() { + return mState == State.UNINITIALIZED || mState == State.FINISH; + } + void onScreenState(int state) { mDozeLog.traceDisplayState(state); for (Part part : mParts) { @@ -360,7 +378,7 @@ public class DozeMachine { if (mState == State.FINISH) { return State.FINISH; } - if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR + if (mUiModeType == Configuration.UI_MODE_TYPE_CAR && (requestedState.canPulse() || requestedState.staysAwake())) { Log.i(TAG, "Doze is suppressed with all triggers disabled as car mode is active"); mDozeLog.traceCarModeStarted(); @@ -411,7 +429,7 @@ public class DozeMachine { nextState = State.FINISH; } else if (mDockManager.isDocked()) { nextState = mDockManager.isHidden() ? State.DOZE : State.DOZE_AOD_DOCKED; - } else if (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) { + } else if (mAmbientDisplayConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) { nextState = State.DOZE_AOD; } else { nextState = State.DOZE; @@ -427,6 +445,7 @@ public class DozeMachine { /** Dumps the current state */ public void dump(PrintWriter pw) { pw.print(" state="); pw.println(mState); + pw.print(" mUiModeType="); pw.println(mUiModeType); pw.print(" wakeLockHeldForCurrentState="); pw.println(mWakeLockHeldForCurrentState); pw.print(" wakeLock="); pw.println(mWakeLock); pw.println("Parts:"); @@ -459,6 +478,19 @@ public class DozeMachine { /** Sets the {@link DozeMachine} when this Part is associated with one. */ default void setDozeMachine(DozeMachine dozeMachine) {} + + /** + * Notifies the Part about a change in {@link Configuration#uiMode}. + * + * @param newUiModeType {@link Configuration#UI_MODE_TYPE_NORMAL}, + * {@link Configuration#UI_MODE_TYPE_DESK}, + * {@link Configuration#UI_MODE_TYPE_CAR}, + * {@link Configuration#UI_MODE_TYPE_TELEVISION}, + * {@link Configuration#UI_MODE_TYPE_APPLIANCE}, + * {@link Configuration#UI_MODE_TYPE_WATCH}, + * or {@link Configuration#UI_MODE_TYPE_VR_HEADSET} + */ + default void onUiModeTypeChanged(int newUiModeType) {} } /** A wrapper interface for {@link android.service.dreams.DreamService} */ diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java index a2eb4e3bb640..e8d7e4642e3e 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java @@ -17,6 +17,7 @@ package com.android.systemui.doze; import android.content.Context; +import android.content.res.Configuration; import android.os.PowerManager; import android.os.SystemClock; import android.service.dreams.DreamService; @@ -59,6 +60,7 @@ public class DozeService extends DreamService mPluginManager.addPluginListener(this, DozeServicePlugin.class, false /* allowMultiple */); DozeComponent dozeComponent = mDozeComponentBuilder.build(this); mDozeMachine = dozeComponent.getDozeMachine(); + mDozeMachine.onConfigurationChanged(getResources().getConfiguration()); } @Override @@ -127,6 +129,12 @@ public class DozeService extends DreamService } @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDozeMachine.onConfigurationChanged(newConfig); + } + + @Override public void onRequestHideDoze() { if (mDozeMachine != null) { mDozeMachine.requestState(DozeMachine.State.DOZE); diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java index 7ed4b35e1ee7..e6d98655b119 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java @@ -16,21 +16,13 @@ package com.android.systemui.doze; -import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE; -import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE; - -import android.app.UiModeManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; +import static android.content.res.Configuration.UI_MODE_TYPE_CAR; + import android.hardware.display.AmbientDisplayConfiguration; import android.os.PowerManager; import android.os.UserHandle; import android.text.TextUtils; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.doze.dagger.DozeScope; import com.android.systemui.statusbar.phone.BiometricUnlockController; @@ -43,7 +35,9 @@ import dagger.Lazy; /** * Handles suppressing doze on: * 1. INITIALIZED, don't allow dozing at all when: - * - in CAR_MODE + * - in CAR_MODE, in this scenario the device is asleep and won't listen for any triggers + * to wake up. In this state, no UI shows. Unlike other conditions, this suppression is only + * temporary and stops when the device exits CAR_MODE * - device is NOT provisioned * - there's a pending authentication * 2. PowerSaveMode active @@ -57,35 +51,47 @@ import dagger.Lazy; */ @DozeScope public class DozeSuppressor implements DozeMachine.Part { - private static final String TAG = "DozeSuppressor"; private DozeMachine mMachine; private final DozeHost mDozeHost; private final AmbientDisplayConfiguration mConfig; private final DozeLog mDozeLog; - private final BroadcastDispatcher mBroadcastDispatcher; - private final UiModeManager mUiModeManager; private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy; - private boolean mBroadcastReceiverRegistered; + private boolean mIsCarModeEnabled = false; @Inject public DozeSuppressor( DozeHost dozeHost, AmbientDisplayConfiguration config, DozeLog dozeLog, - BroadcastDispatcher broadcastDispatcher, - UiModeManager uiModeManager, Lazy<BiometricUnlockController> biometricUnlockControllerLazy) { mDozeHost = dozeHost; mConfig = config; mDozeLog = dozeLog; - mBroadcastDispatcher = broadcastDispatcher; - mUiModeManager = uiModeManager; mBiometricUnlockControllerLazy = biometricUnlockControllerLazy; } @Override + public void onUiModeTypeChanged(int newUiModeType) { + boolean isCarModeEnabled = newUiModeType == UI_MODE_TYPE_CAR; + if (mIsCarModeEnabled == isCarModeEnabled) { + return; + } + mIsCarModeEnabled = isCarModeEnabled; + // Do not handle the event if doze machine is not initialized yet. + // It will be handled upon initialization. + if (mMachine.isUninitializedOrFinished()) { + return; + } + if (mIsCarModeEnabled) { + handleCarModeStarted(); + } else { + handleCarModeExited(); + } + } + + @Override public void setDozeMachine(DozeMachine dozeMachine) { mMachine = dozeMachine; } @@ -94,7 +100,6 @@ public class DozeSuppressor implements DozeMachine.Part { public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) { switch (newState) { case INITIALIZED: - registerBroadcastReceiver(); mDozeHost.addCallback(mHostCallback); checkShouldImmediatelyEndDoze(); checkShouldImmediatelySuspendDoze(); @@ -108,14 +113,12 @@ public class DozeSuppressor implements DozeMachine.Part { @Override public void destroy() { - unregisterBroadcastReceiver(); mDozeHost.removeCallback(mHostCallback); } private void checkShouldImmediatelySuspendDoze() { - if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) { - mDozeLog.traceCarModeStarted(); - mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS); + if (mIsCarModeEnabled) { + handleCarModeStarted(); } } @@ -135,7 +138,7 @@ public class DozeSuppressor implements DozeMachine.Part { @Override public void dump(PrintWriter pw) { - pw.println(" uiMode=" + mUiModeManager.getCurrentModeType()); + pw.println(" isCarModeEnabled=" + mIsCarModeEnabled); pw.println(" hasPendingAuth=" + mBiometricUnlockControllerLazy.get().hasPendingAuthentication()); pw.println(" isProvisioned=" + mDozeHost.isProvisioned()); @@ -143,40 +146,18 @@ public class DozeSuppressor implements DozeMachine.Part { pw.println(" aodPowerSaveActive=" + mDozeHost.isPowerSaveActive()); } - private void registerBroadcastReceiver() { - if (mBroadcastReceiverRegistered) { - return; - } - IntentFilter filter = new IntentFilter(ACTION_ENTER_CAR_MODE); - filter.addAction(ACTION_EXIT_CAR_MODE); - mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter); - mBroadcastReceiverRegistered = true; + private void handleCarModeExited() { + mDozeLog.traceCarModeEnded(); + mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT) + ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE); } - private void unregisterBroadcastReceiver() { - if (!mBroadcastReceiverRegistered) { - return; - } - mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver); - mBroadcastReceiverRegistered = false; + private void handleCarModeStarted() { + mDozeLog.traceCarModeStarted(); + mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS); } - private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (ACTION_ENTER_CAR_MODE.equals(action)) { - mDozeLog.traceCarModeStarted(); - mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS); - } else if (ACTION_EXIT_CAR_MODE.equals(action)) { - mDozeLog.traceCarModeEnded(); - mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT) - ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE); - } - } - }; - - private DozeHost.Callback mHostCallback = new DozeHost.Callback() { + private final DozeHost.Callback mHostCallback = new DozeHost.Callback() { @Override public void onPowerSaveChanged(boolean active) { // handles suppression changes, while DozeMachine#transitionPolicy handles gating diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java index 0ccb222c8acc..cedd850ac2ef 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java @@ -210,7 +210,8 @@ public class DreamHomeControlsComplication implements Complication { final Intent intent = new Intent(mContext, ControlsActivity.class) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(ControlsUiController.EXTRA_ANIMATE, true); + .putExtra(ControlsUiController.EXTRA_ANIMATE, true) + .putExtra(ControlsUiController.EXIT_TO_DREAM, true); final ActivityLaunchAnimator.Controller controller = v != null ? ActivityLaunchAnimator.Controller.fromView(v, null /* cujType */) diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt index 08ef8f3d025f..478f86169718 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt @@ -24,7 +24,7 @@ import com.android.systemui.R import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_HIGH import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL -import com.android.systemui.log.LogBuffer +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager import java.io.PrintWriter import javax.inject.Inject @@ -235,6 +235,7 @@ class DumpHandler @Inject constructor( pw.println("$ <invocation> buffers") pw.println("$ <invocation> bugreport-critical") pw.println("$ <invocation> bugreport-normal") + pw.println("$ <invocation> config") pw.println() pw.println("Targets can be listed:") @@ -313,13 +314,21 @@ class DumpHandler @Inject constructor( const val PRIORITY_ARG_CRITICAL = "CRITICAL" const val PRIORITY_ARG_HIGH = "HIGH" const val PRIORITY_ARG_NORMAL = "NORMAL" + const val PROTO = "--sysui_proto" } } private val PRIORITY_OPTIONS = arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_HIGH, PRIORITY_ARG_NORMAL) -private val COMMANDS = arrayOf("bugreport-critical", "bugreport-normal", "buffers", "dumpables") +private val COMMANDS = arrayOf( + "bugreport-critical", + "bugreport-normal", + "buffers", + "dumpables", + "config", + "help" +) private class ParsedArgs( val rawArgs: Array<String>, diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt index cca04da8f426..dbca65122fcb 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt @@ -18,7 +18,7 @@ package com.android.systemui.dump import android.util.ArrayMap import com.android.systemui.Dumpable -import com.android.systemui.log.LogBuffer +import com.android.systemui.plugins.log.LogBuffer import java.io.PrintWriter import javax.inject.Inject import javax.inject.Singleton diff --git a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt index 0eab1afc4119..8299b13d305f 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt @@ -19,7 +19,7 @@ package com.android.systemui.dump import android.content.Context import android.util.Log import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.util.io.Files import com.android.systemui.util.time.SystemClock import java.io.IOException diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index ab1e8c6010d1..07606c1f015f 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -45,35 +45,45 @@ public class Flags { /***************************************/ // 100 - notification + // TODO(b/254512751): Tracking Bug public static final UnreleasedFlag NOTIFICATION_PIPELINE_DEVELOPER_LOGGING = new UnreleasedFlag(103); + // TODO(b/254512732): Tracking Bug public static final UnreleasedFlag NSSL_DEBUG_LINES = new UnreleasedFlag(105); + // TODO(b/254512505): Tracking Bug public static final UnreleasedFlag NSSL_DEBUG_REMOVE_ANIMATION = new UnreleasedFlag(106); - public static final UnreleasedFlag NEW_PIPELINE_CRASH_ON_CALL_TO_OLD_PIPELINE = - new UnreleasedFlag(107); - + // TODO(b/254512624): Tracking Bug public static final ResourceBooleanFlag NOTIFICATION_DRAG_TO_CONTENTS = new ResourceBooleanFlag(108, R.bool.config_notificationToContents); + // TODO(b/254512703): Tracking Bug public static final ReleasedFlag REMOVE_UNRANKED_NOTIFICATIONS = new ReleasedFlag(109); + // TODO(b/254512517): Tracking Bug public static final UnreleasedFlag FSI_REQUIRES_KEYGUARD = new UnreleasedFlag(110, true); + // TODO(b/254512538): Tracking Bug public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true); + // TODO(b/254512425): Tracking Bug public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112, false); + // TODO(b/254512731): Tracking Bug public static final UnreleasedFlag NOTIFICATION_DISMISSAL_FADE = new UnreleasedFlag(113, true); - // next id: 114 + public static final UnreleasedFlag STABILITY_INDEX_FIX = new UnreleasedFlag(114, true); + + public static final UnreleasedFlag SEMI_STABLE_SORT = new UnreleasedFlag(115, true); + + // next id: 116 /***************************************/ // 200 - keyguard/lockscreen @@ -82,27 +92,33 @@ public class Flags { // public static final BooleanFlag KEYGUARD_LAYOUT = // new BooleanFlag(200, true); + // TODO(b/254512713): Tracking Bug public static final ReleasedFlag LOCKSCREEN_ANIMATIONS = new ReleasedFlag(201); + // TODO(b/254512750): Tracking Bug public static final ReleasedFlag NEW_UNLOCK_SWIPE_ANIMATION = new ReleasedFlag(202); public static final ResourceBooleanFlag CHARGING_RIPPLE = new ResourceBooleanFlag(203, R.bool.flag_charging_ripple); + // TODO(b/254512281): Tracking Bug public static final ResourceBooleanFlag BOUNCER_USER_SWITCHER = new ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher); + // TODO(b/254512694): Tracking Bug public static final ResourceBooleanFlag FACE_SCANNING_ANIM = new ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation); + // TODO(b/254512676): Tracking Bug public static final UnreleasedFlag LOCKSCREEN_CUSTOM_CLOCKS = new UnreleasedFlag(207); /** * Flag to enable the usage of the new bouncer data source. This is a refactor of and * eventual replacement of KeyguardBouncer.java. */ + // TODO(b/254512385): Tracking Bug public static final UnreleasedFlag MODERN_BOUNCER = new UnreleasedFlag(208); /** @@ -111,8 +127,9 @@ public class Flags { * <p>If this is {@code false}, the interactor and repo skip the controller and directly access * the framework APIs. */ - public static final ReleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER = - new ReleasedFlag(210); + // TODO(b/254513286): Tracking Bug + public static final UnreleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER = + new UnreleasedFlag(210); /** * Whether `UserSwitcherController` should use the user interactor. @@ -123,24 +140,28 @@ public class Flags { * <p>Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is * {@code true} as it would created a cycle between controller -> interactor -> controller. */ - public static final UnreleasedFlag USER_CONTROLLER_USES_INTERACTOR = new UnreleasedFlag(211); + // TODO(b/254513102): Tracking Bug + public static final ReleasedFlag USER_CONTROLLER_USES_INTERACTOR = new ReleasedFlag(211); /** * Whether the clock on a wide lock screen should use the new "stepping" animation for moving * the digits when the clock moves. */ - public static final UnreleasedFlag STEP_CLOCK_ANIMATION = new UnreleasedFlag(211); + public static final UnreleasedFlag STEP_CLOCK_ANIMATION = new UnreleasedFlag(212); /***************************************/ // 300 - power menu + // TODO(b/254512600): Tracking Bug public static final ReleasedFlag POWER_MENU_LITE = new ReleasedFlag(300); /***************************************/ // 400 - smartspace + // TODO(b/254513080): Tracking Bug public static final ReleasedFlag SMARTSPACE_DEDUPING = new ReleasedFlag(400); + // TODO(b/254513100): Tracking Bug public static final ReleasedFlag SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED = new ReleasedFlag(401); @@ -156,6 +177,7 @@ public class Flags { public static final ReleasedFlag NEW_USER_SWITCHER = new ReleasedFlag(500); + // TODO(b/254512321): Tracking Bug public static final UnreleasedFlag COMBINED_QS_HEADERS = new UnreleasedFlag(501, true); @@ -168,37 +190,48 @@ public class Flags { /** * @deprecated Not needed anymore */ + // TODO(b/254512699): Tracking Bug @Deprecated public static final ReleasedFlag NEW_FOOTER = new ReleasedFlag(504); + // TODO(b/254512747): Tracking Bug public static final UnreleasedFlag NEW_HEADER = new UnreleasedFlag(505, true); + // TODO(b/254512383): Tracking Bug public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER = new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher); + // TODO(b/254512678): Tracking Bug public static final ReleasedFlag NEW_FOOTER_ACTIONS = new ReleasedFlag(507); /***************************************/ // 600- status bar + // TODO(b/254513246): Tracking Bug public static final ResourceBooleanFlag STATUS_BAR_USER_SWITCHER = new ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip); + // TODO(b/254513025): Tracking Bug public static final ReleasedFlag STATUS_BAR_LETTERBOX_APPEARANCE = new ReleasedFlag(603, false); + // TODO(b/254512623): Tracking Bug public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_BACKEND = new UnreleasedFlag(604, false); + // TODO(b/254512660): Tracking Bug public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_FRONTEND = new UnreleasedFlag(605, false); /***************************************/ // 700 - dialer/calls + // TODO(b/254512734): Tracking Bug public static final ReleasedFlag ONGOING_CALL_STATUS_BAR_CHIP = new ReleasedFlag(700); + // TODO(b/254512681): Tracking Bug public static final ReleasedFlag ONGOING_CALL_IN_IMMERSIVE = new ReleasedFlag(701); + // TODO(b/254512753): Tracking Bug public static final ReleasedFlag ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP = new ReleasedFlag(702); @@ -209,33 +242,49 @@ public class Flags { /***************************************/ // 801 - region sampling + // TODO(b/254512848): Tracking Bug public static final UnreleasedFlag REGION_SAMPLING = new UnreleasedFlag(801); // 802 - wallpaper rendering + // TODO(b/254512923): Tracking Bug public static final UnreleasedFlag USE_CANVAS_RENDERER = new UnreleasedFlag(802, true); // 803 - screen contents translation + // TODO(b/254513187): Tracking Bug public static final UnreleasedFlag SCREEN_CONTENTS_TRANSLATION = new UnreleasedFlag(803); + // 804 - monochromatic themes + public static final UnreleasedFlag MONOCHROMATIC_THEMES = new UnreleasedFlag(804); + /***************************************/ // 900 - media + // TODO(b/254512697): Tracking Bug public static final ReleasedFlag MEDIA_TAP_TO_TRANSFER = new ReleasedFlag(900); + // TODO(b/254512502): Tracking Bug public static final UnreleasedFlag MEDIA_SESSION_ACTIONS = new UnreleasedFlag(901); + // TODO(b/254512726): Tracking Bug public static final ReleasedFlag MEDIA_NEARBY_DEVICES = new ReleasedFlag(903); + // TODO(b/254512695): Tracking Bug public static final ReleasedFlag MEDIA_MUTE_AWAIT = new ReleasedFlag(904); + // TODO(b/254512654): Tracking Bug public static final UnreleasedFlag DREAM_MEDIA_COMPLICATION = new UnreleasedFlag(905); + // TODO(b/254512673): Tracking Bug public static final UnreleasedFlag DREAM_MEDIA_TAP_TO_OPEN = new UnreleasedFlag(906); + // TODO(b/254513168): Tracking Bug public static final UnreleasedFlag UMO_SURFACE_RIPPLE = new UnreleasedFlag(907); // 1000 - dock public static final ReleasedFlag SIMULATE_DOCK_THROUGH_CHARGING = new ReleasedFlag(1000); + + // TODO(b/254512444): Tracking Bug public static final ReleasedFlag DOCK_SETUP_ENABLED = new ReleasedFlag(1001); - public static final UnreleasedFlag ROUNDED_BOX_RIPPLE = - new UnreleasedFlag(1002, /* teamfood= */ true); + // TODO(b/254512758): Tracking Bug + public static final ReleasedFlag ROUNDED_BOX_RIPPLE = new ReleasedFlag(1002); - public static final UnreleasedFlag REFACTORED_DOCK_SETUP = new UnreleasedFlag(1003, true); + // TODO(b/254512525): Tracking Bug + public static final ReleasedFlag REFACTORED_DOCK_SETUP = new ReleasedFlag(1003); // 1100 - windowing @Keep @@ -249,11 +298,13 @@ public class Flags { public static final SysPropBooleanFlag BUBBLES_HOME_GESTURE = new SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true); + // TODO(b/254513207): Tracking Bug @Keep public static final DeviceConfigBooleanFlag WM_ENABLE_PARTIAL_SCREEN_SHARING = new DeviceConfigBooleanFlag(1102, "record_task_content", NAMESPACE_WINDOW_MANAGER, false, true); + // TODO(b/254512674): Tracking Bug @Keep public static final SysPropBooleanFlag HIDE_NAVBAR_WINDOW = new SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false); @@ -296,18 +347,23 @@ public class Flags { public static final SysPropBooleanFlag WM_ALWAYS_ENFORCE_PREDICTIVE_BACK = new SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false); + // TODO(b/254512728): Tracking Bug public static final UnreleasedFlag NEW_BACK_AFFORDANCE = new UnreleasedFlag(1203, false /* teamfood */); // 1300 - screenshots + // TODO(b/254512719): Tracking Bug public static final UnreleasedFlag SCREENSHOT_REQUEST_PROCESSOR = new UnreleasedFlag(1300); + // TODO(b/254513155): Tracking Bug public static final UnreleasedFlag SCREENSHOT_WORK_PROFILE_POLICY = new UnreleasedFlag(1301); // 1400 - columbus + // TODO(b/254512756): Tracking Bug public static final ReleasedFlag QUICK_TAP_IN_PCC = new ReleasedFlag(1400); // 1500 - chooser + // TODO(b/254512507): Tracking Bug public static final UnreleasedFlag CHOOSER_UNBUNDLED = new UnreleasedFlag(1500); // 1600 - accessibility @@ -317,6 +373,10 @@ public class Flags { // 1700 - clipboard public static final UnreleasedFlag CLIPBOARD_OVERLAY_REFACTOR = new UnreleasedFlag(1700); + // 1800 - shade container + public static final UnreleasedFlag LEAVE_SHADE_OPEN_FOR_BUGREPORT = + new UnreleasedFlag(1800, true); + // Pay no attention to the reflection behind the curtain. // ========================== Curtain ========================== // | | diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index da5819a7f3bc..3ef5499237f1 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -116,6 +116,7 @@ import com.android.systemui.MultiListLayout; import com.android.systemui.MultiListLayout.MultiListAdapter; import com.android.systemui.animation.DialogCuj; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.animation.Expandable; import com.android.systemui.animation.Interpolators; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; @@ -448,10 +449,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene * * @param keyguardShowing True if keyguard is showing * @param isDeviceProvisioned True if device is provisioned - * @param view The view from which we should animate the dialog when showing it + * @param expandable The expandable from which we should animate the dialog when + * showing it */ public void showOrHideDialog(boolean keyguardShowing, boolean isDeviceProvisioned, - @Nullable View view) { + @Nullable Expandable expandable) { mKeyguardShowing = keyguardShowing; mDeviceProvisioned = isDeviceProvisioned; if (mDialog != null && mDialog.isShowing()) { @@ -463,7 +465,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mDialog.dismiss(); mDialog = null; } else { - handleShow(view); + handleShow(expandable); } } @@ -495,7 +497,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } } - protected void handleShow(@Nullable View view) { + protected void handleShow(@Nullable Expandable expandable) { awakenIfNecessary(); mDialog = createDialog(); prepareDialog(); @@ -507,10 +509,12 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene // Don't acquire soft keyboard focus, to avoid destroying state when capturing bugreports mDialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM); - if (view != null) { - mDialogLaunchAnimator.showFromView(mDialog, view, - new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG)); + DialogLaunchAnimator.Controller controller = + expandable != null ? expandable.dialogLaunchController( + new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG)) : null; + if (controller != null) { + mDialogLaunchAnimator.show(mDialog, controller); } else { mDialog.show(); } @@ -1016,8 +1020,9 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene Log.w(TAG, "Bugreport handler could not be launched"); mIActivityManager.requestInteractiveBugReport(); } - // Close shade so user sees the activity - mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade); + // Maybe close shade (depends on a flag) so user sees the activity + mCentralSurfacesOptional.ifPresent( + CentralSurfaces::collapseShadeForBugreport); } catch (RemoteException e) { } } @@ -1036,8 +1041,8 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mMetricsLogger.action(MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_FULL); mUiEventLogger.log(GlobalActionsEvent.GA_BUGREPORT_LONG_PRESS); mIActivityManager.requestFullBugReport(); - // Close shade so user sees the activity - mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade); + // Maybe close shade (depends on a flag) so user sees the activity + mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShadeForBugreport); } catch (RemoteException e) { } return false; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 84bd8cec51c8..ea26c4dc8a65 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -751,6 +751,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (DEBUG) Log.d(TAG, "keyguardGone"); mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false); mKeyguardDisplayManager.hide(); + mUpdateMonitor.startBiometricWatchdog(); Trace.endSection(); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 56f1ac46a875..56a1f1ae936e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -43,6 +43,7 @@ import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; +import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.statusbar.NotificationShadeDepthController; @@ -72,6 +73,7 @@ import dagger.Provides; FalsingModule.class, KeyguardQuickAffordanceModule.class, KeyguardRepositoryModule.class, + StartKeyguardTransitionModule.class, }) public class KeyguardModule { /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 45b668e609ea..b186ae0ceec4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -21,6 +21,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.common.shared.model.Position import com.android.systemui.dagger.SysUISingleton import com.android.systemui.doze.DozeHost +import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject @@ -85,6 +86,9 @@ interface KeyguardRepository { */ val dozeAmount: Flow<Float> + /** Observable for the [StatusBarState] */ + val statusBarState: Flow<StatusBarState> + /** * Returns `true` if the keyguard is showing; `false` otherwise. * @@ -185,6 +189,24 @@ constructor( return keyguardStateController.isShowing } + override val statusBarState: Flow<StatusBarState> = conflatedCallbackFlow { + val callback = + object : StatusBarStateController.StateListener { + override fun onStateChanged(state: Int) { + trySendWithFailureLogging(statusBarStateIntToObject(state), TAG, "state") + } + } + + statusBarStateController.addCallback(callback) + trySendWithFailureLogging( + statusBarStateIntToObject(statusBarStateController.getState()), + TAG, + "initial state" + ) + + awaitClose { statusBarStateController.removeCallback(callback) } + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.value = animate } @@ -197,6 +219,15 @@ constructor( _clockPosition.value = Position(x, y) } + private fun statusBarStateIntToObject(value: Int): StatusBarState { + return when (value) { + 0 -> StatusBarState.SHADE + 1 -> StatusBarState.KEYGUARD + 2 -> StatusBarState.SHADE_LOCKED + else -> throw IllegalArgumentException("Invalid StatusBarState value: $value") + } + } + companion object { private const val TAG = "KeyguardRepositoryImpl" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt new file mode 100644 index 000000000000..e8532ecfdc37 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.data.repository + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.animation.ValueAnimator.AnimatorUpdateListener +import android.annotation.FloatRange +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter + +@SysUISingleton +class KeyguardTransitionRepository @Inject constructor() { + /* + * Each transition between [KeyguardState]s will have an associated Flow. + * In order to collect these events, clients should call [transition]. + */ + private val _transitions = MutableStateFlow(TransitionStep()) + val transitions = _transitions.asStateFlow() + + /* Information about the active transition. */ + private var currentTransitionInfo: TransitionInfo? = null + /* + * When manual control of the transition is requested, a unique [UUID] is used as the handle + * to permit calls to [updateTransition] + */ + private var updateTransitionId: UUID? = null + + /** + * Interactors that require information about changes between [KeyguardState]s will call this to + * register themselves for flowable [TransitionStep]s when that transition occurs. + */ + fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> { + return transitions.filter { step -> step.from == from && step.to == to } + } + + /** + * Begin a transition from one state to another. The [info.from] must match + * [currentTransitionInfo.to], or the request will be denied. This is enforced to avoid + * unplanned transitions. + */ + fun startTransition(info: TransitionInfo): UUID? { + if (currentTransitionInfo != null) { + // Open questions: + // * Queue of transitions? buffer of 1? + // * Are transitions cancellable if a new one is triggered? + // * What validation does this need to do? + Log.wtf(TAG, "Transition still active: $currentTransitionInfo") + return null + } + currentTransitionInfo?.animator?.cancel() + + currentTransitionInfo = info + info.animator?.let { animator -> + // An animator was provided, so use it to run the transition + animator.setFloatValues(0f, 1f) + val updateListener = + object : AnimatorUpdateListener { + override fun onAnimationUpdate(animation: ValueAnimator) { + emitTransition( + info, + (animation.getAnimatedValue() as Float), + TransitionState.RUNNING + ) + } + } + val adapter = + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + Log.i(TAG, "Starting transition: $info") + emitTransition(info, 0f, TransitionState.STARTED) + } + override fun onAnimationCancel(animation: Animator) { + Log.i(TAG, "Cancelling transition: $info") + } + override fun onAnimationEnd(animation: Animator) { + Log.i(TAG, "Ending transition: $info") + emitTransition(info, 1f, TransitionState.FINISHED) + animator.removeListener(this) + animator.removeUpdateListener(updateListener) + } + } + animator.addListener(adapter) + animator.addUpdateListener(updateListener) + animator.start() + return@startTransition null + } + ?: run { + Log.i(TAG, "Starting transition (manual): $info") + emitTransition(info, 0f, TransitionState.STARTED) + + // No animator, so it's manual. Provide a mechanism to callback + updateTransitionId = UUID.randomUUID() + return@startTransition updateTransitionId + } + } + + /** + * Allows manual control of a transition. When calling [startTransition], the consumer must pass + * in a null animator. In return, it will get a unique [UUID] that will be validated to allow + * further updates. + * + * When the transition is over, TransitionState.FINISHED must be passed into the [state] + * parameter. + */ + fun updateTransition( + transitionId: UUID, + @FloatRange(from = 0.0, to = 1.0) value: Float, + state: TransitionState + ) { + if (updateTransitionId != transitionId) { + Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId") + return + } + + if (currentTransitionInfo == null) { + Log.wtf(TAG, "Attempting to update with null 'currentTransitionInfo'") + return + } + + currentTransitionInfo?.let { info -> + if (state == TransitionState.FINISHED) { + updateTransitionId = null + Log.i(TAG, "Ending transition: $info") + } + + emitTransition(info, value, state) + } + } + + private fun emitTransition( + info: TransitionInfo, + value: Float, + transitionState: TransitionState + ) { + if (transitionState == TransitionState.FINISHED) { + currentTransitionInfo = null + } + _transitions.value = TransitionStep(info.from, info.to, value, transitionState) + } + + companion object { + private const val TAG = "KeyguardTransitionRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt new file mode 100644 index 000000000000..400376663f1a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.animation.ValueAnimator +import com.android.systemui.animation.Interpolators +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@SysUISingleton +class AodLockscreenTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, + private val keyguardTransitionRepository: KeyguardTransitionRepository, +) : TransitionInteractor("AOD<->LOCKSCREEN") { + + override fun start() { + scope.launch { + keyguardRepository.isDozing.collect { isDozing -> + if (isDozing) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + getAnimator(), + ) + ) + } else { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + getAnimator(), + ) + ) + } + } + } + } + + private fun getAnimator(): ValueAnimator { + return ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(TRANSITION_DURATION_MS) + } + } + + companion object { + private const val TRANSITION_DURATION_MS = 500L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 192919e32cf6..fc2269c6b01c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -38,7 +38,7 @@ constructor( val dozeAmount: Flow<Float> = repository.dozeAmount /** Whether the system is in doze mode. */ val isDozing: Flow<Boolean> = repository.isDozing - /** Whether the keyguard is showing ot not. */ + /** Whether the keyguard is showing to not. */ val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing fun isKeyguardShowing(): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt new file mode 100644 index 000000000000..b166681433a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.domain.interactor + +import android.util.Log +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import java.util.Set +import javax.inject.Inject + +@SysUISingleton +class KeyguardTransitionCoreStartable +@Inject +constructor( + private val interactors: Set<TransitionInteractor>, +) : CoreStartable { + + override fun start() { + // By listing the interactors in a when, the compiler will help enforce all classes + // extending the sealed class [TransitionInteractor] will be initialized. + interactors.forEach { + // `when` needs to be an expression in order for the compiler to enforce it being + // exhaustive + val ret = + when (it) { + is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it") + is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it") + } + it.start() + } + } + + companion object { + private const val TAG = "KeyguardTransitionCoreStartable" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt new file mode 100644 index 000000000000..59bb22786917 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionStep +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** Encapsulates business-logic related to the keyguard transitions. */ +@SysUISingleton +class KeyguardTransitionInteractor +@Inject +constructor( + repository: KeyguardTransitionRepository, +) { + /** AOD->LOCKSCREEN transition information. */ + val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt new file mode 100644 index 000000000000..3c2a12e3836a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.util.kotlin.sample +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +@SysUISingleton +class LockscreenBouncerTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, + private val shadeRepository: ShadeRepository, + private val keyguardTransitionRepository: KeyguardTransitionRepository, +) : TransitionInteractor("LOCKSCREEN<->BOUNCER") { + + private var transitionId: UUID? = null + + override fun start() { + scope.launch { + shadeRepository.shadeModel.sample( + combine( + keyguardTransitionRepository.transitions, + keyguardRepository.statusBarState, + ) { transitions, statusBarState -> + Pair(transitions, statusBarState) + } + ) { shadeModel, pair -> + val (transitions, statusBarState) = pair + + val id = transitionId + if (id != null) { + // An existing `id` means a transition is started, and calls to + // `updateTransition` will control it until FINISHED + keyguardTransitionRepository.updateTransition( + id, + shadeModel.expansionAmount, + if (shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f) { + transitionId = null + TransitionState.FINISHED + } else { + TransitionState.RUNNING + } + ) + } else { + // TODO (b/251849525): Remove statusbarstate check when that state is integrated + // into KeyguardTransitionRepository + val isOnLockscreen = + transitions.transitionState == TransitionState.FINISHED && + transitions.to == KeyguardState.LOCKSCREEN + if ( + isOnLockscreen && + shadeModel.isUserDragging && + statusBarState != SHADE_LOCKED + ) { + transitionId = + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = name, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.BOUNCER, + animator = null, + ) + ) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt new file mode 100644 index 000000000000..74c542c0043f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import dagger.multibindings.IntoSet + +@Module +abstract class StartKeyguardTransitionModule { + + @Binds + @IntoMap + @ClassKey(KeyguardTransitionCoreStartable::class) + abstract fun bind(impl: KeyguardTransitionCoreStartable): CoreStartable + + @Binds + @IntoSet + abstract fun lockscreenBouncer( + impl: LockscreenBouncerTransitionInteractor + ): TransitionInteractor + + @Binds + @IntoSet + abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt new file mode 100644 index 000000000000..a2a46d9e3a71 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.keyguard.domain.interactor +/** + * Each TransitionInteractor is responsible for determining under which conditions to notify + * [KeyguardTransitionRepository] to signal a transition. When (and if) the transition occurs is + * determined by [KeyguardTransitionRepository]. + * + * [name] field should be a unique identifiable string representing this state, used primarily for + * logging + * + * MUST list implementing classes in dagger module [StartKeyguardTransitionModule] and also in the + * 'when' clause of [KeyguardTransitionCoreStartable] + */ +sealed class TransitionInteractor(val name: String) { + + abstract fun start() +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt new file mode 100644 index 000000000000..f66d5d3650c8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +/** List of all possible states to transition to/from */ +enum class KeyguardState { + /** For initialization only */ + NONE, + /* Always-on Display. The device is in a low-power mode with a minimal UI visible */ + AOD, + /* + * The security screen prompt UI, containing PIN, Password, Pattern, and all FPS + * (Fingerprint Sensor) variations, for the user to verify their credentials + */ + BOUNCER, + /* + * Device is actively displaying keyguard UI and is not in low-power mode. Device may be + * unlocked if SWIPE security method is used, or if face lockscreen bypass is false. + */ + LOCKSCREEN, +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt new file mode 100644 index 000000000000..bb953477583d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +/** See [com.android.systemui.statusbar.StatusBarState] for definitions */ +enum class StatusBarState { + SHADE, + KEYGUARD, + SHADE_LOCKED, +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt new file mode 100644 index 000000000000..bfccf3fe076c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +import android.animation.ValueAnimator + +/** Tracks who is controlling the current transition, and how to run it. */ +data class TransitionInfo( + val ownerName: String, + val from: KeyguardState, + val to: KeyguardState, + val animator: ValueAnimator?, // 'null' animator signal manual control +) { + override fun toString(): String = + "TransitionInfo(ownerName=$ownerName, from=$from, to=$to, " + + (if (animator != null) { + "animated" + } else { + "manual" + }) + + ")" +} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt index 23072a231417..d8691c17f53d 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt @@ -11,11 +11,14 @@ * 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. + * limitations under the License */ +package com.android.systemui.keyguard.shared.model -package com.android.settingslib.spa.gallery - -import com.android.settingslib.spa.framework.DebugActivity - -class GalleryDebugActivity : DebugActivity() +/** Possible states for a running transition between [State] */ +enum class TransitionState { + NONE, + STARTED, + RUNNING, + FINISHED +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt new file mode 100644 index 000000000000..688ec912aac8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +/** This information will flow from the [KeyguardTransitionRepository] to control the UI layer */ +data class TransitionStep( + val from: KeyguardState = KeyguardState.NONE, + val to: KeyguardState = KeyguardState.NONE, + val value: Float = 0f, // constrained [0.0, 1.0] + val transitionState: TransitionState = TransitionState.NONE, +) diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt index 5651399cb891..f9e341c8629a 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt @@ -19,6 +19,9 @@ package com.android.systemui.log import android.app.ActivityManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogcatEchoTracker + import javax.inject.Inject @SysUISingleton @@ -26,7 +29,7 @@ class LogBufferFactory @Inject constructor( private val dumpManager: DumpManager, private val logcatEchoTracker: LogcatEchoTracker ) { - /* limit the size of maxPoolSize for low ram (Go) devices */ + /* limitiometricMessageDeferralLogger the size of maxPoolSize for low ram (Go) devices */ private fun adjustMaxSize(requestedMaxSize: Int): Int { return if (ActivityManager.isLowRamDeviceStatic()) { minOf(requestedMaxSize, 20) /* low ram max log size*/ diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java index 7f1ad6d20c16..eeadf406060d 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java @@ -23,7 +23,7 @@ import java.lang.annotation.RetentionPolicy; import javax.inject.Qualifier; /** - * A {@link com.android.systemui.log.LogBuffer} for BiometricMessages processing such as + * A {@link com.android.systemui.plugins.log.LogBuffer} for BiometricMessages processing such as * {@link com.android.systemui.biometrics.FaceHelpMessageDeferral} */ @Qualifier diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java index 7d1f1c2709fa..5cca1ab2abe7 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java index 9ca0293fbd86..1d016d837b02 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java index 7c5f4025117f..c9f78bcdeef8 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java index 08d969b5eb77..76d20bea4bdf 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 28aa19e18e80..00bf2104b7f2 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -22,11 +22,11 @@ import android.os.Looper; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.log.LogBuffer; import com.android.systemui.log.LogBufferFactory; -import com.android.systemui.log.LogcatEchoTracker; -import com.android.systemui.log.LogcatEchoTrackerDebug; -import com.android.systemui.log.LogcatEchoTrackerProd; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogcatEchoTracker; +import com.android.systemui.plugins.log.LogcatEchoTrackerDebug; +import com.android.systemui.plugins.log.LogcatEchoTrackerProd; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.util.Compile; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java index 1d7ba94af4ed..90ced0291805 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java index b03655a543f7..e5ac3e2e893b 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java index c67d8bebe313..73690ab6c24d 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java index 53963fc8d431..99ec05bc8d94 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java index 5c572e8ef554..1570d434bc62 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java index edab8c319f87..bf216c6991d2 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java index 75a34fc22c3c..8c904eab409e 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java index b1c6dcfcb13b..6d91f0c97c8a 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java index 20fc6ff445a6..26af4964f7b8 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java index fcc184a317b8..61daf9c8d71c 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java index 760fbf3928b6..a59afa0fed1b 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java index a0b686487bec..6f8ea7ff2e9b 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java index 8c8753a07339..835d3490293c 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java index 7259eebf19b6..6e2bd7b2e1b5 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java index e96e532f94bf..77b1bf5fd630 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java index 557a254e5c09..9fd166b759d2 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java index dd5010cf39a8..dd168bac5654 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java index bd0d298ebdee..d24bfcb88188 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java index b237f2d74483..67cdb722055b 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java index f26b3164f488..af0f7c518e64 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java index dd6837563a74..4c276e2bfaab 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java index 8671dbfdf1fe..ba8b27c23ec1 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt index b1018f9544c0..d40624bfc63a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt @@ -17,9 +17,9 @@ package com.android.systemui.media import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.MediaCarouselControllerLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** A debug logger for [MediaCarouselController]. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt index 1ac2a078c8a0..be357ee0ff73 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt @@ -182,8 +182,7 @@ class MediaProjectionAppSelectorActivity( override fun shouldGetOnlyDefaultActivities() = false - // TODO(b/240924732) flip the flag when the recents selector is ready - override fun shouldShowContentPreview() = false + override fun shouldShowContentPreview() = true override fun createContentPreviewView(parent: ViewGroup): ViewGroup = recentsViewController.createView(parent) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt index b52565d57f27..cc06b6c67879 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt @@ -33,7 +33,6 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager -import com.android.systemui.people.widget.PeopleSpaceWidgetProvider.EXTRA_USER_HANDLE import com.android.systemui.tuner.TunerService import com.android.systemui.util.Utils import com.android.systemui.util.time.SystemClock diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt index d9c58c0d0d76..8c9e2d88c694 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt @@ -18,11 +18,10 @@ package com.android.systemui.media import android.media.session.PlaybackState import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.MediaTimeoutListenerLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject - private const val TAG = "MediaTimeout" /** diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt index 73868189b362..51c658cb6c54 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt @@ -17,9 +17,9 @@ package com.android.systemui.media import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.MediaViewLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject private const val TAG = "MediaView" diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt index 41f735486c7e..a9c5c61dddbb 100644 --- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt @@ -18,9 +18,9 @@ package com.android.systemui.media import android.content.ComponentName import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.MediaBrowserLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** A logger for events in [ResumeMediaBrowser]. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java index a8a84331050d..e15e2d3dcccf 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java @@ -17,7 +17,6 @@ package com.android.systemui.media.dagger; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.log.LogBuffer; import com.android.systemui.log.dagger.MediaTttReceiverLogBuffer; import com.android.systemui.log.dagger.MediaTttSenderLogBuffer; import com.android.systemui.media.MediaDataManager; @@ -33,6 +32,7 @@ import com.android.systemui.media.taptotransfer.MediaTttFlags; import com.android.systemui.media.taptotransfer.common.MediaTttLogger; import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger; import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger; +import com.android.systemui.plugins.log.LogBuffer; import java.util.Optional; diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt index 78f4e012da03..5ace3ea8a05b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt @@ -1,9 +1,9 @@ package com.android.systemui.media.muteawait import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.MediaMuteAwaitLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** Log messages for [MediaMuteAwaitConnectionManager]. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt index 46b2cc141b3c..78408fce5a36 100644 --- a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt @@ -1,9 +1,9 @@ package com.android.systemui.media.nearby import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NearbyMediaDevicesLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** Log messages for [NearbyMediaDevicesManager]. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt index b565f3c22f24..38c971ed3f7d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt @@ -16,8 +16,8 @@ package com.android.systemui.media.taptotransfer.common -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.temporarydisplay.TemporaryViewLogger /** diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt index c3de94f28aea..0a6043793ef6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt @@ -21,6 +21,8 @@ import android.content.pm.PackageManager import android.graphics.drawable.Drawable import com.android.settingslib.Utils import com.android.systemui.R +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon /** Utility methods for media tap-to-transfer. */ class MediaTttUtils { @@ -31,6 +33,23 @@ class MediaTttUtils { const val WAKE_REASON = "MEDIA_TRANSFER_ACTIVATED" /** + * Returns the information needed to display the icon in [Icon] form. + * + * See [getIconInfoFromPackageName]. + */ + fun getIconFromPackageName( + context: Context, + appPackageName: String?, + logger: MediaTttLogger, + ): Icon { + val iconInfo = getIconInfoFromPackageName(context, appPackageName, logger) + return Icon.Loaded( + iconInfo.drawable, + ContentDescription.Loaded(iconInfo.contentDescription) + ) + } + + /** * Returns the information needed to display the icon. * * The information will either contain app name and icon of the app playing media, or a diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt index c24b0307fcd1..6e596ee1f473 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt @@ -18,17 +18,12 @@ package com.android.systemui.media.taptotransfer.sender import android.app.StatusBarManager import android.content.Context -import android.media.MediaRoute2Info import android.util.Log -import android.view.View import androidx.annotation.StringRes import com.android.internal.logging.UiEventLogger -import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R -import com.android.systemui.plugins.FalsingManager +import com.android.systemui.common.shared.model.Text import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS -import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo -import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator /** * A class enumerating all the possible states of the media tap-to-transfer chip on the sender @@ -38,6 +33,7 @@ import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator * @property stringResId the res ID of the string that should be displayed in the chip. Null if the * state should not have the chip be displayed. * @property transferStatus the transfer status that the chip state represents. + * @property endItem the item that should be displayed in the end section of the chip. * @property timeout the amount of time this chip should display on the screen before it times out * and disappears. */ @@ -46,6 +42,7 @@ enum class ChipStateSender( val uiEvent: UiEventLogger.UiEventEnum, @StringRes val stringResId: Int?, val transferStatus: TransferStatus, + val endItem: SenderEndItem?, val timeout: Long = DEFAULT_TIMEOUT_MILLIS ) { /** @@ -58,6 +55,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST, R.string.media_move_closer_to_start_cast, transferStatus = TransferStatus.NOT_STARTED, + endItem = null, ), /** @@ -71,6 +69,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST, R.string.media_move_closer_to_end_cast, transferStatus = TransferStatus.NOT_STARTED, + endItem = null, ), /** @@ -82,6 +81,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED, R.string.media_transfer_playing_different_device, transferStatus = TransferStatus.IN_PROGRESS, + endItem = SenderEndItem.Loading, timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS ), @@ -94,6 +94,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED, R.string.media_transfer_playing_this_device, transferStatus = TransferStatus.IN_PROGRESS, + endItem = SenderEndItem.Loading, timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS ), @@ -105,36 +106,13 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED, R.string.media_transfer_playing_different_device, transferStatus = TransferStatus.SUCCEEDED, - ) { - override fun undoClickListener( - chipbarCoordinator: ChipbarCoordinator, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger, - falsingManager: FalsingManager, - ): View.OnClickListener? { - if (undoCallback == null) { - return null - } - return View.OnClickListener { - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener - - uiEventLogger.logUndoClicked( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED - ) - undoCallback.onUndoTriggered() - // The external service should eventually send us a TransferToThisDeviceTriggered - // state, but that may take too long to go through the binder and the user may be - // confused as to why the UI hasn't changed yet. So, we immediately change the UI - // here. - chipbarCoordinator.displayView( - ChipSenderInfo( - TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo, undoCallback - ) - ) - } - } - }, + endItem = SenderEndItem.UndoButton( + uiEventOnClick = + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED, + newState = + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED + ), + ), /** * A state representing that a transfer back to this device has been successfully completed. @@ -144,36 +122,13 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, R.string.media_transfer_playing_this_device, transferStatus = TransferStatus.SUCCEEDED, - ) { - override fun undoClickListener( - chipbarCoordinator: ChipbarCoordinator, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger, - falsingManager: FalsingManager, - ): View.OnClickListener? { - if (undoCallback == null) { - return null - } - return View.OnClickListener { - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener - - uiEventLogger.logUndoClicked( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED - ) - undoCallback.onUndoTriggered() - // The external service should eventually send us a TransferToReceiverTriggered - // state, but that may take too long to go through the binder and the user may be - // confused as to why the UI hasn't changed yet. So, we immediately change the UI - // here. - chipbarCoordinator.displayView( - ChipSenderInfo( - TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo, undoCallback - ) - ) - } - } - }, + endItem = SenderEndItem.UndoButton( + uiEventOnClick = + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED, + newState = + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED + ), + ), /** A state representing that a transfer to the receiver device has failed. */ TRANSFER_TO_RECEIVER_FAILED( @@ -181,6 +136,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED, R.string.media_transfer_failed, transferStatus = TransferStatus.FAILED, + endItem = SenderEndItem.Error, ), /** A state representing that a transfer back to this device has failed. */ @@ -189,6 +145,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED, R.string.media_transfer_failed, transferStatus = TransferStatus.FAILED, + endItem = SenderEndItem.Error, ), /** A state representing that this device is far away from any receiver device. */ @@ -197,37 +154,27 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER, stringResId = null, transferStatus = TransferStatus.TOO_FAR, - ); + // We shouldn't be displaying the chipbar anyway + endItem = null, + ) { + override fun getChipTextString(context: Context, otherDeviceName: String): Text { + // TODO(b/245610654): Better way to handle this. + throw IllegalArgumentException("FAR_FROM_RECEIVER should never be displayed, " + + "so its string should never be fetched") + } + }; /** * Returns a fully-formed string with the text that the chip should display. * + * Throws an NPE if [stringResId] is null. + * * @param otherDeviceName the name of the other device involved in the transfer. */ - fun getChipTextString(context: Context, otherDeviceName: String): String? { - if (stringResId == null) { - return null - } - return context.getString(stringResId, otherDeviceName) + open fun getChipTextString(context: Context, otherDeviceName: String): Text { + return Text.Loaded(context.getString(stringResId!!, otherDeviceName)) } - /** - * Returns a click listener for the undo button on the chip. Returns null if this chip state - * doesn't have an undo button. - * - * @param chipbarCoordinator passed as a parameter in case we want to display a new chipbar - * when undo is clicked. - * @param undoCallback if present, the callback that should be called when the user clicks the - * undo button. The undo button will only be shown if this is non-null. - */ - open fun undoClickListener( - chipbarCoordinator: ChipbarCoordinator, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger, - falsingManager: FalsingManager, - ): View.OnClickListener? = null - companion object { /** * Returns the sender state enum associated with the given [displayState] from @@ -253,6 +200,26 @@ enum class ChipStateSender( } } +/** Represents the item that should be displayed in the end section of the chip. */ +sealed class SenderEndItem { + /** A loading icon should be displayed. */ + object Loading : SenderEndItem() + + /** An error icon should be displayed. */ + object Error : SenderEndItem() + + /** + * An undo button should be displayed. + * + * @property uiEventOnClick the UI event to log when this button is clicked. + * @property newState the state that should immediately be transitioned to. + */ + data class UndoButton( + val uiEventOnClick: UiEventLogger.UiEventEnum, + @StatusBarManager.MediaTransferSenderState val newState: Int, + ) : SenderEndItem() +} + // Give the Transfer*Triggered states a longer timeout since those states represent an active // process and we should keep the user informed about it as long as possible (but don't allow it to // continue indefinitely). diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt index 224303ac098c..fe2eed9c6079 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt @@ -20,14 +20,20 @@ import android.app.StatusBarManager import android.content.Context import android.media.MediaRoute2Info import android.util.Log +import android.view.View +import com.android.internal.logging.UiEventLogger import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.CoreStartable +import com.android.systemui.R +import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.taptotransfer.MediaTttFlags import com.android.systemui.media.taptotransfer.common.MediaTttLogger +import com.android.systemui.media.taptotransfer.common.MediaTttUtils import com.android.systemui.statusbar.CommandQueue -import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator +import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem +import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo import com.android.systemui.temporarydisplay.chipbar.SENDER_TAG import javax.inject.Inject @@ -107,7 +113,89 @@ constructor( chipbarCoordinator.removeView(removalReason) } else { displayedState = chipState - chipbarCoordinator.displayView(ChipSenderInfo(chipState, routeInfo, undoCallback)) + chipbarCoordinator.displayView( + createChipbarInfo( + chipState, + routeInfo, + undoCallback, + context, + logger, + ) + ) } } + + /** + * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display. + */ + private fun createChipbarInfo( + chipStateSender: ChipStateSender, + routeInfo: MediaRoute2Info, + undoCallback: IUndoMediaTransferCallback?, + context: Context, + logger: MediaTttLogger, + ): ChipbarInfo { + val packageName = routeInfo.clientPackageName + val otherDeviceName = routeInfo.name.toString() + + return ChipbarInfo( + // Display the app's icon as the start icon + startIcon = MediaTttUtils.getIconFromPackageName(context, packageName, logger), + text = chipStateSender.getChipTextString(context, otherDeviceName), + endItem = + when (chipStateSender.endItem) { + null -> null + is SenderEndItem.Loading -> ChipbarEndItem.Loading + is SenderEndItem.Error -> ChipbarEndItem.Error + is SenderEndItem.UndoButton -> { + if (undoCallback != null) { + getUndoButton( + undoCallback, + chipStateSender.endItem.uiEventOnClick, + chipStateSender.endItem.newState, + routeInfo, + ) + } else { + null + } + } + } + ) + } + + /** + * Returns an undo button for the chip. + * + * When the button is clicked: [undoCallback] will be triggered, [uiEvent] will be logged, and + * this coordinator will transition to [newState]. + */ + private fun getUndoButton( + undoCallback: IUndoMediaTransferCallback, + uiEvent: UiEventLogger.UiEventEnum, + @StatusBarManager.MediaTransferSenderState newState: Int, + routeInfo: MediaRoute2Info, + ): ChipbarEndItem.Button { + val onClickListener = + View.OnClickListener { + uiEventLogger.logUndoClicked(uiEvent) + undoCallback.onUndoTriggered() + + // The external service should eventually send us a new TransferTriggered state, but + // but that may take too long to go through the binder and the user may be confused + // as to why the UI hasn't changed yet. So, we immediately change the UI here. + updateMediaTapToTransferSenderDisplay( + newState, + routeInfo, + // Since we're force-updating the UI, we don't have any [undoCallback] from the + // external service (and TransferTriggered states don't have undo callbacks + // anyway). + undoCallback = null, + ) + } + + return ChipbarEndItem.Button( + Text.Resource(R.string.media_transfer_undo), + onClickListener, + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt index 1ea93474f954..03503fd1ff61 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt @@ -17,10 +17,10 @@ package com.android.systemui.privacy.logging import android.permission.PermissionGroupUsage -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.PrivacyLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogMessage import com.android.systemui.privacy.PrivacyDialog import com.android.systemui.privacy.PrivacyItem import java.text.SimpleDateFormat diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 482a1397642b..bb2b4419a80a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -52,6 +52,7 @@ import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -98,10 +99,10 @@ interface FgsManagerController { fun init() /** - * Show the foreground services dialog. The dialog will be expanded from [viewLaunchedFrom] if + * Show the foreground services dialog. The dialog will be expanded from [expandable] if * it's not `null`. */ - fun showDialog(viewLaunchedFrom: View?) + fun showDialog(expandable: Expandable?) /** Add a [OnNumberOfPackagesChangedListener]. */ fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) @@ -367,7 +368,7 @@ class FgsManagerControllerImpl @Inject constructor( override fun shouldUpdateFooterVisibility() = dialog == null - override fun showDialog(viewLaunchedFrom: View?) { + override fun showDialog(expandable: Expandable?) { synchronized(lock) { if (dialog == null) { @@ -403,16 +404,18 @@ class FgsManagerControllerImpl @Inject constructor( } mainExecutor.execute { - viewLaunchedFrom - ?.let { - dialogLaunchAnimator.showFromView( - dialog, it, - cuj = DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG - ) + val controller = + expandable?.dialogLaunchController( + DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG, ) - } ?: dialog.show() + ) + if (controller != null) { + dialogLaunchAnimator.show(dialog, controller) + } else { + dialog.show() + } } backgroundExecutor.execute { diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt index 9d64781ef2e9..a9943e886339 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt @@ -32,6 +32,7 @@ import com.android.internal.logging.nano.MetricsProto import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.globalactions.GlobalActionsDialogLite import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager @@ -156,7 +157,7 @@ internal class FooterActionsController @Inject constructor( startSettingsActivity() } else if (v === powerMenuLite) { uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS) - globalActionsDialog?.showOrHideDialog(false, true, v) + globalActionsDialog?.showOrHideDialog(false, true, Expandable.fromView(powerMenuLite)) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java index 7511278e0919..b1b9dd721eaf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java @@ -29,6 +29,7 @@ import android.widget.TextView; import androidx.annotation.Nullable; import com.android.systemui.R; +import com.android.systemui.animation.Expandable; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.qs.dagger.QSScope; @@ -130,7 +131,7 @@ public class QSFgsManagerFooter implements View.OnClickListener, @Override public void onClick(View view) { - mFgsManagerController.showDialog(mRootView); + mFgsManagerController.showDialog(Expandable.fromView(view)); } public void refreshState() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt index e5d86cc96f25..025fb228b829 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.qs -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.QSFragmentDisableLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.disableflags.DisableFlagsLogger import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java index 67bf3003deff..6c1e95645550 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java @@ -39,6 +39,7 @@ import androidx.annotation.Nullable; import com.android.internal.util.FrameworkStatsLog; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; +import com.android.systemui.animation.Expandable; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.common.shared.model.Icon; import com.android.systemui.dagger.qualifiers.Background; @@ -169,7 +170,7 @@ public class QSSecurityFooter extends ViewController<View> // TODO(b/242040009): Remove this. public void showDeviceMonitoringDialog() { - mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, mView); + mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, Expandable.fromView(mView)); } public void refreshState() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java index ae6ed2008a77..67bc76998597 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java @@ -75,6 +75,7 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.systemui.R; import com.android.systemui.animation.DialogCuj; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.animation.Expandable; import com.android.systemui.common.shared.model.ContentDescription; import com.android.systemui.common.shared.model.Icon; import com.android.systemui.dagger.SysUISingleton; @@ -190,8 +191,9 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { } /** Show the device monitoring dialog. */ - public void showDeviceMonitoringDialog(Context quickSettingsContext, @Nullable View view) { - createDialog(quickSettingsContext, view); + public void showDeviceMonitoringDialog(Context quickSettingsContext, + @Nullable Expandable expandable) { + createDialog(quickSettingsContext, expandable); } /** @@ -440,7 +442,7 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { } } - private void createDialog(Context quickSettingsContext, @Nullable View view) { + private void createDialog(Context quickSettingsContext, @Nullable Expandable expandable) { mShouldUseSettingsButton.set(false); mBgHandler.post(() -> { String settingsButtonText = getSettingsButton(); @@ -453,9 +455,12 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { ? settingsButtonText : getNegativeButton(), this); mDialog.setView(dialogView); - if (view != null && view.isAggregatedVisible()) { - mDialogLaunchAnimator.showFromView(mDialog, view, new DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)); + DialogLaunchAnimator.Controller controller = + expandable != null ? expandable.dialogLaunchController(new DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)) + : null; + if (controller != null) { + mDialogLaunchAnimator.show(mDialog, controller); } else { mDialog.show(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java index 4cacbbacec2f..5d03da3cc113 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java @@ -35,6 +35,7 @@ import androidx.annotation.Nullable; import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.qs.QSTileHost; import com.android.systemui.settings.UserTracker; @@ -53,6 +54,7 @@ import javax.inject.Provider; /** * Runs the day-to-day operations of which tiles should be bound and when. */ +@SysUISingleton public class TileServices extends IQSService.Stub { static final int DEFAULT_MAX_BOUND = 3; static final int REDUCED_MAX_BOUND = 1; diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt index cf9b41c25388..9ba3501c3434 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt @@ -23,13 +23,11 @@ import android.content.Intent import android.content.IntentFilter import android.os.UserHandle import android.provider.Settings -import android.view.View import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.MetricsLogger import com.android.internal.logging.UiEventLogger import com.android.internal.logging.nano.MetricsProto import com.android.internal.util.FrameworkStatsLog -import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.Expandable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton @@ -74,37 +72,27 @@ interface FooterActionsInteractor { val deviceMonitoringDialogRequests: Flow<Unit> /** - * Show the device monitoring dialog, expanded from [view]. - * - * Important: [view] must be associated to the same [Context] as the [Quick Settings fragment] - * [com.android.systemui.qs.QSFragment]. - */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showDeviceMonitoringDialog(view: View) - - /** - * Show the device monitoring dialog. + * Show the device monitoring dialog, expanded from [expandable] if it's not null. * * Important: [quickSettingsContext] *must* be the [Context] associated to the [Quick Settings * fragment][com.android.systemui.qs.QSFragment]. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showDeviceMonitoringDialog(quickSettingsContext: Context) + fun showDeviceMonitoringDialog(quickSettingsContext: Context, expandable: Expandable?) /** Show the foreground services dialog. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showForegroundServicesDialog(view: View) + fun showForegroundServicesDialog(expandable: Expandable) /** Show the power menu dialog. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) + fun showPowerMenuDialog( + globalActionsDialogLite: GlobalActionsDialogLite, + expandable: Expandable, + ) /** Show the settings. */ fun showSettings(expandable: Expandable) /** Show the user switcher. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showUserSwitcher(view: View) + fun showUserSwitcher(context: Context, expandable: Expandable) } @SysUISingleton @@ -147,28 +135,32 @@ constructor( null, ) - override fun showDeviceMonitoringDialog(view: View) { - qsSecurityFooterUtils.showDeviceMonitoringDialog(view.context, view) - DevicePolicyEventLogger.createEvent( - FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED - ) - .write() - } - - override fun showDeviceMonitoringDialog(quickSettingsContext: Context) { - qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, /* view= */ null) + override fun showDeviceMonitoringDialog( + quickSettingsContext: Context, + expandable: Expandable?, + ) { + qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, expandable) + if (expandable != null) { + DevicePolicyEventLogger.createEvent( + FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED + ) + .write() + } } - override fun showForegroundServicesDialog(view: View) { - fgsManagerController.showDialog(view) + override fun showForegroundServicesDialog(expandable: Expandable) { + fgsManagerController.showDialog(expandable) } - override fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) { + override fun showPowerMenuDialog( + globalActionsDialogLite: GlobalActionsDialogLite, + expandable: Expandable, + ) { uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS) globalActionsDialogLite.showOrHideDialog( /* keyguardShowing= */ false, /* isDeviceProvisioned= */ true, - view, + expandable, ) } @@ -189,21 +181,21 @@ constructor( ) } - override fun showUserSwitcher(view: View) { + override fun showUserSwitcher(context: Context, expandable: Expandable) { if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { - userSwitchDialogController.showDialog(view) + userSwitchDialogController.showDialog(context, expandable) return } val intent = - Intent(view.context, UserSwitcherActivity::class.java).apply { + Intent(context, UserSwitcherActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) } activityStarter.startActivity( intent, true /* dismissShade */, - ActivityLaunchAnimator.Controller.fromView(view, null), + expandable.activityLaunchController(), true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt index dd1ffcc9fa12..3e39c8ee62f1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt @@ -31,6 +31,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.R +import com.android.systemui.animation.Expandable import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.people.ui.view.PeopleViewBinder.bind @@ -125,7 +126,7 @@ object FooterActionsViewBinder { launch { viewModel.security.collect { security -> if (previousSecurity != security) { - bindSecurity(securityHolder, security) + bindSecurity(view.context, securityHolder, security) previousSecurity = security } } @@ -159,6 +160,7 @@ object FooterActionsViewBinder { } private fun bindSecurity( + quickSettingsContext: Context, securityHolder: TextButtonViewHolder, security: FooterActionsSecurityButtonViewModel?, ) { @@ -171,9 +173,12 @@ object FooterActionsViewBinder { // Make sure that the chevron is visible and that the button is clickable if there is a // listener. val chevron = securityHolder.chevron - if (security.onClick != null) { + val onClick = security.onClick + if (onClick != null) { securityView.isClickable = true - securityView.setOnClickListener(security.onClick) + securityView.setOnClickListener { + onClick(quickSettingsContext, Expandable.fromView(securityView)) + } chevron.isVisible = true } else { securityView.isClickable = false @@ -205,7 +210,9 @@ object FooterActionsViewBinder { foregroundServicesWithNumberView.isVisible = false foregroundServicesWithTextView.isVisible = true - foregroundServicesWithTextView.setOnClickListener(foregroundServices.onClick) + foregroundServicesWithTextView.setOnClickListener { + foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView)) + } foregroundServicesWithTextHolder.text.text = foregroundServices.text foregroundServicesWithTextHolder.newDot.isVisible = foregroundServices.hasNewChanges } else { @@ -213,7 +220,9 @@ object FooterActionsViewBinder { foregroundServicesWithTextView.isVisible = false foregroundServicesWithNumberView.visibility = View.VISIBLE - foregroundServicesWithNumberView.setOnClickListener(foregroundServices.onClick) + foregroundServicesWithNumberView.setOnClickListener { + foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView)) + } foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString() foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text foregroundServicesWithNumberHolder.newDot.isVisible = foregroundServices.hasNewChanges @@ -229,7 +238,7 @@ object FooterActionsViewBinder { } buttonView.setBackgroundResource(model.background) - buttonView.setOnClickListener(model.onClick) + buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) } val icon = model.icon val iconView = button.icon diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt index 9b5f683d8dab..8d819dacba67 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt @@ -17,7 +17,7 @@ package com.android.systemui.qs.footer.ui.viewmodel import android.annotation.DrawableRes -import android.view.View +import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon /** @@ -29,7 +29,5 @@ data class FooterActionsButtonViewModel( val icon: Icon, val iconTint: Int?, @DrawableRes val background: Int, - // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog - // or activity. - val onClick: (View) -> Unit, + val onClick: (Expandable) -> Unit, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt index 98b53cb0ed5a..ff8130d3e6ec 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt @@ -16,7 +16,7 @@ package com.android.systemui.qs.footer.ui.viewmodel -import android.view.View +import com.android.systemui.animation.Expandable /** A ViewModel for the foreground services button. */ data class FooterActionsForegroundServicesButtonViewModel( @@ -24,5 +24,5 @@ data class FooterActionsForegroundServicesButtonViewModel( val text: String, val displayText: Boolean, val hasNewChanges: Boolean, - val onClick: (View) -> Unit, + val onClick: (Expandable) -> Unit, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt index 98ab129fc9de..3450505f9f86 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt @@ -16,12 +16,13 @@ package com.android.systemui.qs.footer.ui.viewmodel -import android.view.View +import android.content.Context +import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon /** A ViewModel for the security button. */ data class FooterActionsSecurityButtonViewModel( val icon: Icon, val text: String, - val onClick: ((View) -> Unit)?, + val onClick: ((quickSettingsContext: Context, Expandable) -> Unit)?, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt index d3c06f60bc90..dee6fadbc9cb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.qs.footer.ui.viewmodel import android.content.Context import android.util.Log -import android.view.View import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner @@ -199,50 +198,51 @@ class FooterActionsViewModel( */ suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) { footerActionsInteractor.deviceMonitoringDialogRequests.collect { - footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext) + footerActionsInteractor.showDeviceMonitoringDialog( + quickSettingsContext, + expandable = null, + ) } } - private fun onSecurityButtonClicked(view: View) { + private fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showDeviceMonitoringDialog(view) + footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext, expandable) } - private fun onForegroundServiceButtonClicked(view: View) { + private fun onForegroundServiceButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showForegroundServicesDialog(view) + footerActionsInteractor.showForegroundServicesDialog(expandable) } - private fun onUserSwitcherClicked(view: View) { + private fun onUserSwitcherClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showUserSwitcher(view) + footerActionsInteractor.showUserSwitcher(context, expandable) } - // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog - // or activity. - private fun onSettingsButtonClicked(view: View) { + private fun onSettingsButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showSettings(Expandable.fromView(view)) + footerActionsInteractor.showSettings(expandable) } - private fun onPowerButtonClicked(view: View) { + private fun onPowerButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, view) + footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, expandable) } private fun userSwitcherButton( diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt index 60380064e098..931dc8df151a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt @@ -17,12 +17,12 @@ package com.android.systemui.qs.logging import android.service.quicksettings.Tile -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.VERBOSE -import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.QSLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.VERBOSE +import com.android.systemui.plugins.log.LogMessage import com.android.systemui.plugins.qs.QSTile import com.android.systemui.statusbar.StatusBarState import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java index d2d5063c7ae0..b6b657ec82f6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java @@ -26,6 +26,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.logging.MetricsLogger; @@ -43,6 +44,9 @@ import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.user.data.source.UserRecord; +import java.util.List; +import java.util.stream.Collectors; + import javax.inject.Inject; /** @@ -83,6 +87,13 @@ public class UserDetailView extends PseudoGridView { private final FalsingManager mFalsingManager; private @Nullable UserSwitchDialogController.DialogShower mDialogShower; + @NonNull + @Override + protected List<UserRecord> getUsers() { + return super.getUsers().stream().filter( + userRecord -> !userRecord.isManageUsers).collect(Collectors.toList()); + } + @Inject public Adapter(Context context, UserSwitcherController controller, UiEventLogger uiEventLogger, FalsingManager falsingManager) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt index bdcc6b0b2a57..314252bf310b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt @@ -23,13 +23,13 @@ import android.content.DialogInterface.BUTTON_NEUTRAL import android.content.Intent import android.provider.Settings import android.view.LayoutInflater -import android.view.View import androidx.annotation.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.UiEventLogger import com.android.systemui.R import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager @@ -77,10 +77,10 @@ class UserSwitchDialogController @VisibleForTesting constructor( * Show a [UserDialog]. * * Populate the dialog with information from and adapter obtained from - * [userDetailViewAdapterProvider] and show it as launched from [view]. + * [userDetailViewAdapterProvider] and show it as launched from [expandable]. */ - fun showDialog(view: View) { - with(dialogFactory(view.context)) { + fun showDialog(context: Context, expandable: Expandable) { + with(dialogFactory(context)) { setShowForAllUsers(true) setCanceledOnTouchOutside(true) @@ -112,13 +112,19 @@ class UserSwitchDialogController @VisibleForTesting constructor( adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid)) - dialogLaunchAnimator.showFromView( - this, view, - cuj = DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG + val controller = + expandable.dialogLaunchController( + DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) ) - ) + if (controller != null) { + dialogLaunchAnimator.show( + this, + controller, + ) + } else { + show() + } + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN) adapter.injectDialogShower(DialogShowerImpl(this, dialogLaunchAnimator)) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt index a494f42985ac..6b540aa9f392 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt @@ -292,6 +292,7 @@ class LargeScreenShadeHeaderController @Inject constructor( clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ -> val newPivot = if (v.isLayoutRtl) v.width.toFloat() else 0f v.pivotX = newPivot + v.pivotY = v.height.toFloat() / 2 } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt index 07e8b9fe3123..754036d3baa9 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt @@ -16,7 +16,7 @@ package com.android.systemui.shade import android.view.MotionEvent import com.android.systemui.dump.DumpsysTableLogger import com.android.systemui.dump.Row -import com.android.systemui.util.collection.RingBuffer +import com.android.systemui.plugins.util.RingBuffer import java.text.SimpleDateFormat import java.util.Locale diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 29d546fe78c5..8433ee8a7ec8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1443,6 +1443,16 @@ public final class NotificationPanelViewController { mMaxAllowedKeyguardNotifications = maxAllowed; } + @VisibleForTesting + boolean getClosing() { + return mClosing; + } + + @VisibleForTesting + boolean getIsFlinging() { + return mIsFlinging; + } + private void updateMaxDisplayedNotifications(boolean recompute) { if (recompute) { setMaxDisplayedNotifications(Math.max(computeMaxKeyguardNotifications(), 1)); @@ -1675,9 +1685,9 @@ public final class NotificationPanelViewController { transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); boolean customClockAnimation = - mKeyguardStatusViewController - .getClockAnimations() - .getHasCustomPositionUpdatedAnimation(); + mKeyguardStatusViewController.getClockAnimations() != null + && mKeyguardStatusViewController.getClockAnimations() + .getHasCustomPositionUpdatedAnimation(); if (mFeatureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION) && customClockAnimation) { // Find the clock, so we can exclude it from this transition. @@ -2127,7 +2137,8 @@ public final class NotificationPanelViewController { animator.start(); } - private void onFlingEnd(boolean cancelled) { + @VisibleForTesting + void onFlingEnd(boolean cancelled) { mIsFlinging = false; // No overshoot when the animation ends setOverExpansionInternal(0, false /* isFromGesture */); @@ -3717,6 +3728,11 @@ public final class NotificationPanelViewController { setListening(true); } + @VisibleForTesting + void setTouchSlopExceeded(boolean isTouchSlopExceeded) { + mTouchSlopExceeded = isTouchSlopExceeded; + } + public void setOverExpansion(float overExpansion) { if (overExpansion == mOverExpansion) { return; @@ -3868,12 +3884,14 @@ public final class NotificationPanelViewController { } } - private void setIsClosing(boolean isClosing) { + @VisibleForTesting + void setIsClosing(boolean isClosing) { boolean wasClosing = isClosing(); mClosing = isClosing; if (wasClosing != isClosing) { mPanelEventsEmitter.notifyPanelCollapsingChanged(isClosing); } + mAmbientState.setIsClosing(isClosing); } private void updateDozingVisibilities(boolean animate) { @@ -3903,17 +3921,17 @@ public final class NotificationPanelViewController { switch (mBarState) { case KEYGUARD: if (!mDozingOnDown) { - if (mUpdateMonitor.isFaceEnrolled() - && !mUpdateMonitor.isFaceDetectionRunning() - && !mUpdateMonitor.getUserCanSkipBouncer( - KeyguardUpdateMonitor.getCurrentUser())) { - mUpdateMonitor.requestFaceAuth(true, - FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED); - } else { - mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT, - 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */); - mLockscreenGestureLogger - .log(LockscreenUiEvent.LOCKSCREEN_LOCK_SHOW_HINT); + mShadeLog.v("onMiddleClicked on Keyguard, mDozingOnDown: false"); + // Try triggering face auth, this "might" run. Check + // KeyguardUpdateMonitor#shouldListenForFace to see when face auth won't run. + mUpdateMonitor.requestFaceAuth(true, + FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED); + + mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT, + 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */); + mLockscreenGestureLogger + .log(LockscreenUiEvent.LOCKSCREEN_LOCK_SHOW_HINT); + if (!mUpdateMonitor.isFaceDetectionRunning()) { startUnlockHintAnimation(); } if (mUpdateMonitor.isFaceEnrolled()) { @@ -4664,14 +4682,16 @@ public final class NotificationPanelViewController { Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args)); } - private void notifyExpandingStarted() { + @VisibleForTesting + void notifyExpandingStarted() { if (!mExpanding) { mExpanding = true; onExpandingStarted(); } } - private void notifyExpandingFinished() { + @VisibleForTesting + void notifyExpandingFinished() { endClosing(); if (mExpanding) { mExpanding = false; @@ -4771,6 +4791,7 @@ public final class NotificationPanelViewController { mAmbientState.setSwipingUp(false); if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop || Math.abs(y - mInitialExpandY) > mTouchSlop + || (!isFullyExpanded() && !isFullyCollapsed()) || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) { mVelocityTracker.computeCurrentVelocity(1000); float vel = mVelocityTracker.getYVelocity(); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt index 7bee0ba17afc..2b788d85a14c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt @@ -1,10 +1,10 @@ package com.android.systemui.shade import android.view.MotionEvent -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.ShadeLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogMessage import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject @@ -12,64 +12,69 @@ private const val TAG = "systemui.shade" /** Lightweight logging utility for the Shade. */ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { - fun v(@CompileTimeConstant msg: String) { - buffer.log(TAG, LogLevel.VERBOSE, msg) - } + fun v(@CompileTimeConstant msg: String) { + buffer.log(TAG, LogLevel.VERBOSE, msg) + } - private inline fun log( - logLevel: LogLevel, - initializer: LogMessage.() -> Unit, - noinline printer: LogMessage.() -> String - ) { - buffer.log(TAG, logLevel, initializer, printer) - } + private inline fun log( + logLevel: LogLevel, + initializer: LogMessage.() -> Unit, + noinline printer: LogMessage.() -> String + ) { + buffer.log(TAG, logLevel, initializer, printer) + } - fun onQsInterceptMoveQsTrackingEnabled(h: Float) { - log( - LogLevel.VERBOSE, - { double1 = h.toDouble() }, - { "onQsIntercept: move action, QS tracking enabled. h = $double1" }) - } + fun onQsInterceptMoveQsTrackingEnabled(h: Float) { + log( + LogLevel.VERBOSE, + { double1 = h.toDouble() }, + { "onQsIntercept: move action, QS tracking enabled. h = $double1" } + ) + } - fun logQsTrackingNotStarted( - initialTouchY: Float, - y: Float, - h: Float, - touchSlop: Float, - qsExpanded: Boolean, - collapsedOnDown: Boolean, - keyguardShowing: Boolean, - qsExpansionEnabled: Boolean - ) { - log( - LogLevel.VERBOSE, - { - int1 = initialTouchY.toInt() - int2 = y.toInt() - long1 = h.toLong() - double1 = touchSlop.toDouble() - bool1 = qsExpanded - bool2 = collapsedOnDown - bool3 = keyguardShowing - bool4 = qsExpansionEnabled - }, - { - "QsTrackingNotStarted: initTouchY=$int1,y=$int2,h=$long1,slop=$double1,qsExpanded=" + - "$bool1,collapsedDown=$bool2,keyguardShowing=$bool3,qsExpansion=$bool4" - }) - } + fun logQsTrackingNotStarted( + initialTouchY: Float, + y: Float, + h: Float, + touchSlop: Float, + qsExpanded: Boolean, + collapsedOnDown: Boolean, + keyguardShowing: Boolean, + qsExpansionEnabled: Boolean + ) { + log( + LogLevel.VERBOSE, + { + int1 = initialTouchY.toInt() + int2 = y.toInt() + long1 = h.toLong() + double1 = touchSlop.toDouble() + bool1 = qsExpanded + bool2 = collapsedOnDown + bool3 = keyguardShowing + bool4 = qsExpansionEnabled + }, + { + "QsTrackingNotStarted: initTouchY=$int1,y=$int2,h=$long1,slop=$double1,qsExpanded" + + "=$bool1,collapsedDown=$bool2,keyguardShowing=$bool3,qsExpansion=$bool4" + } + ) + } - fun logMotionEvent(event: MotionEvent, message: String) { - log( - LogLevel.VERBOSE, - { - str1 = message - long1 = event.eventTime - long2 = event.downTime - int1 = event.action - int2 = event.classification - double1 = event.y.toDouble() - }, - { "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,classification=$int2" }) - } + fun logMotionEvent(event: MotionEvent, message: String) { + log( + LogLevel.VERBOSE, + { + str1 = message + long1 = event.eventTime + long2 = event.downTime + int1 = event.action + int2 = event.classification + double1 = event.y.toDouble() + }, + { + "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,class=$int2" + } + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt new file mode 100644 index 000000000000..09019a69df47 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.shade.data.repository + +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shade.ShadeExpansionChangeEvent +import com.android.systemui.shade.ShadeExpansionListener +import com.android.systemui.shade.ShadeExpansionStateManager +import com.android.systemui.shade.domain.model.ShadeModel +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +/** Business logic for shade interactions */ +@SysUISingleton +class ShadeRepository @Inject constructor(shadeExpansionStateManager: ShadeExpansionStateManager) { + + val shadeModel: Flow<ShadeModel> = + conflatedCallbackFlow { + val callback = + object : ShadeExpansionListener { + override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) { + // Don't propagate ShadeExpansionChangeEvent.dragDownPxAmount field. + // It is too noisy and produces extra events that consumers won't care + // about + val info = + ShadeModel( + expansionAmount = event.fraction, + isExpanded = event.expanded, + isUserDragging = event.tracking + ) + trySendWithFailureLogging(info, TAG, "updated shade expansion info") + } + } + + shadeExpansionStateManager.addExpansionListener(callback) + trySendWithFailureLogging(ShadeModel(), TAG, "initial shade expansion info") + + awaitClose { shadeExpansionStateManager.removeExpansionListener(callback) } + } + .distinctUntilChanged() + + companion object { + private const val TAG = "ShadeRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt new file mode 100644 index 000000000000..ce0f4283ff83 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.shade.domain.model + +import android.annotation.FloatRange + +/** Information about shade (NotificationPanel) expansion */ +data class ShadeModel( + /** 0 when collapsed, 1 when fully expanded. */ + @FloatRange(from = 0.0, to = 1.0) val expansionAmount: Float = 0f, + /** Whether the panel should be considered expanded */ + val isExpanded: Boolean = false, + /** Whether the user is actively dragging the panel. */ + val isUserDragging: Boolean = false, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt index 7f7ff9cf4881..90c52bd8c9f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt @@ -17,9 +17,9 @@ package com.android.systemui.statusbar import android.app.PendingIntent -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotifInteractionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.collection.NotificationEntry import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java index ea7ec4f7fc39..450b757295bc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java @@ -71,9 +71,9 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; -import com.android.systemui.log.LogBuffer; -import com.android.systemui.log.LogLevel; import com.android.systemui.log.dagger.StatusBarNetworkControllerLog; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogLevel; import com.android.systemui.qs.tiles.dialog.InternetDialogFactory; import com.android.systemui.settings.CurrentUserTracker; import com.android.systemui.statusbar.policy.ConfigurationController; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt index 17feaa842165..9bdff928c44b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.gesture -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.SwipeStatusBarAwayLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** Log messages for [SwipeStatusBarAwayGestureHandler]. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index dfba8cd4a081..fc984618f1b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -119,6 +119,7 @@ class LockscreenSmartspaceController @Inject constructor( regionSamplingEnabled, updateFun ) + initializeTextColors(regionSamplingInstance) regionSamplingInstance.startRegionSampler() regionSamplingInstances.put(v, regionSamplingInstance) connectSession() @@ -362,18 +363,20 @@ class LockscreenSmartspaceController @Inject constructor( } } + private fun initializeTextColors(regionSamplingInstance: RegionSamplingInstance) { + val lightThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_LightWallpaper) + val darkColor = Utils.getColorAttrDefaultColor(lightThemeContext, R.attr.wallpaperTextColor) + + val darkThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI) + val lightColor = Utils.getColorAttrDefaultColor(darkThemeContext, R.attr.wallpaperTextColor) + + regionSamplingInstance.setForegroundColors(lightColor, darkColor) + } + private fun updateTextColorFromRegionSampler() { smartspaceViews.forEach { - val isRegionDark = regionSamplingInstances.getValue(it).currentRegionDarkness() - val themeID = if (isRegionDark.isDark) { - R.style.Theme_SystemUI - } else { - R.style.Theme_SystemUI_LightWallpaper - } - val themedContext = ContextThemeWrapper(context, themeID) - val wallpaperTextColor = - Utils.getColorAttrDefaultColor(themedContext, R.attr.wallpaperTextColor) - it.setPrimaryTextColor(wallpaperTextColor) + val textColor = regionSamplingInstances.getValue(it).currentForegroundColor() + it.setPrimaryTextColor(textColor) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt index 7fbdd35796c1..36b8333688ae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt @@ -17,30 +17,14 @@ package com.android.systemui.statusbar.notification import android.content.Context -import android.util.Log -import android.widget.Toast import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.util.Compile import javax.inject.Inject class NotifPipelineFlags @Inject constructor( val context: Context, val featureFlags: FeatureFlags ) { - fun checkLegacyPipelineEnabled(): Boolean { - if (Compile.IS_DEBUG) { - Toast.makeText(context, "Old pipeline code running!", Toast.LENGTH_SHORT).show() - } - if (featureFlags.isEnabled(Flags.NEW_PIPELINE_CRASH_ON_CALL_TO_OLD_PIPELINE)) { - throw RuntimeException("Old pipeline code running with new pipeline enabled") - } else { - Log.d("NotifPipeline", "Old pipeline code running with new pipeline enabled", - Exception()) - } - return false - } - fun isDevLoggingEnabled(): Boolean = featureFlags.isEnabled(Flags.NOTIFICATION_PIPELINE_DEVELOPER_LOGGING) @@ -53,4 +37,12 @@ class NotifPipelineFlags @Inject constructor( fun fullScreenIntentRequiresKeyguard(): Boolean = featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD) + + val isStabilityIndexFixEnabled: Boolean by lazy { + featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX) + } + + val isSemiStableSortEnabled: Boolean by lazy { + featureFlags.isEnabled(Flags.SEMI_STABLE_SORT) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt index ad3dfedcdb96..3058fbbc1031 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotifInteractionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.collection.NotificationEntry import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt index f8449ae8807b..84ab0d1190f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt @@ -68,6 +68,9 @@ data class ListAttachState private constructor( */ var stableIndex: Int = -1 + /** Access the index of the [section] or -1 if the entry does not have one */ + val sectionIndex: Int get() = section?.index ?: -1 + /** Copies the state of another instance. */ fun clone(other: ListAttachState) { parent = other.parent @@ -95,11 +98,13 @@ data class ListAttachState private constructor( * This can happen if the entry is removed from a group that was broken up or if the entry was * filtered out during any of the filtering steps. */ - fun detach() { + fun detach(includingStableIndex: Boolean) { parent = null section = null promoter = null - // stableIndex = -1 // TODO(b/241229236): Clear this once we fix the stability fragility + if (includingStableIndex) { + stableIndex = -1 + } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java index e129ee45817a..3ae2545e4e10 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java @@ -54,6 +54,9 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener; import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener; import com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState; +import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort; +import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort.StableOrder; +import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper; import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator; @@ -96,11 +99,14 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated // TODO replace temp with collection pool for readability private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>(); + private NotifPipelineFlags mFlags; private final boolean mAlwaysLogList; private List<ListEntry> mNotifList = new ArrayList<>(); private List<ListEntry> mNewNotifList = new ArrayList<>(); + private final SemiStableSort mSemiStableSort = new SemiStableSort(); + private final StableOrder<ListEntry> mStableOrder = this::getStableOrderRank; private final PipelineState mPipelineState = new PipelineState(); private final Map<String, GroupEntry> mGroups = new ArrayMap<>(); private Collection<NotificationEntry> mAllEntries = Collections.emptyList(); @@ -141,6 +147,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { ) { mSystemClock = systemClock; mLogger = logger; + mFlags = flags; mAlwaysLogList = flags.isDevLoggingEnabled(); mInteractionTracker = interactionTracker; mChoreographer = pipelineChoreographer; @@ -527,7 +534,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { List<NotifFilter> filters) { Trace.beginSection("ShadeListBuilder.filterNotifs"); final long now = mSystemClock.uptimeMillis(); - for (ListEntry entry : entries) { + for (ListEntry entry : entries) { if (entry instanceof GroupEntry) { final GroupEntry groupEntry = (GroupEntry) entry; @@ -958,7 +965,8 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { * filtered out during any of the filtering steps. */ private void annulAddition(ListEntry entry) { - entry.getAttachState().detach(); + // NOTE(b/241229236): Don't clear stableIndex until we fix stability fragility + entry.getAttachState().detach(/* includingStableIndex= */ mFlags.isSemiStableSortEnabled()); } private void assignSections() { @@ -978,7 +986,16 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { private void sortListAndGroups() { Trace.beginSection("ShadeListBuilder.sortListAndGroups"); - // Assign sections to top-level elements and sort their children + if (mFlags.isSemiStableSortEnabled()) { + sortWithSemiStableSort(); + } else { + sortWithLegacyStability(); + } + Trace.endSection(); + } + + private void sortWithLegacyStability() { + // Sort all groups and the top level list for (ListEntry entry : mNotifList) { if (entry instanceof GroupEntry) { GroupEntry parent = (GroupEntry) entry; @@ -991,16 +1008,15 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // Check for suppressed order changes if (!getStabilityManager().isEveryChangeAllowed()) { mForceReorderable = true; - boolean isSorted = isShadeSorted(); + boolean isSorted = isShadeSortedLegacy(); mForceReorderable = false; if (!isSorted) { getStabilityManager().onEntryReorderSuppressed(); } } - Trace.endSection(); } - private boolean isShadeSorted() { + private boolean isShadeSortedLegacy() { if (!isSorted(mNotifList, mTopLevelComparator)) { return false; } @@ -1014,6 +1030,43 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { return true; } + private void sortWithSemiStableSort() { + // Sort each group's children + boolean allSorted = true; + for (ListEntry entry : mNotifList) { + if (entry instanceof GroupEntry) { + GroupEntry parent = (GroupEntry) entry; + allSorted &= sortGroupChildren(parent.getRawChildren()); + } + } + // Sort each section within the top level list + mNotifList.sort(mTopLevelComparator); + if (!getStabilityManager().isEveryChangeAllowed()) { + for (List<ListEntry> subList : getSectionSubLists(mNotifList)) { + allSorted &= mSemiStableSort.stabilizeTo(subList, mStableOrder, mNewNotifList); + } + applyNewNotifList(); + } + assignIndexes(mNotifList); + if (!allSorted) { + // Report suppressed order changes + getStabilityManager().onEntryReorderSuppressed(); + } + } + + private Iterable<List<ListEntry>> getSectionSubLists(List<ListEntry> entries) { + return ShadeListBuilderHelper.INSTANCE.getSectionSubLists(entries); + } + + private boolean sortGroupChildren(List<NotificationEntry> entries) { + if (getStabilityManager().isEveryChangeAllowed()) { + entries.sort(mGroupChildrenComparator); + return true; + } else { + return mSemiStableSort.sort(entries, mStableOrder, mGroupChildrenComparator); + } + } + /** Determine whether the items in the list are sorted according to the comparator */ @VisibleForTesting public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) { @@ -1036,27 +1089,41 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { /** * Assign the index of each notification relative to the total order */ - private static void assignIndexes(List<ListEntry> notifList) { + private void assignIndexes(List<ListEntry> notifList) { if (notifList.size() == 0) return; NotifSection currentSection = requireNonNull(notifList.get(0).getSection()); int sectionMemberIndex = 0; for (int i = 0; i < notifList.size(); i++) { - ListEntry entry = notifList.get(i); + final ListEntry entry = notifList.get(i); NotifSection section = requireNonNull(entry.getSection()); if (section.getIndex() != currentSection.getIndex()) { sectionMemberIndex = 0; currentSection = section; } - entry.getAttachState().setStableIndex(sectionMemberIndex); - if (entry instanceof GroupEntry) { - GroupEntry parent = (GroupEntry) entry; - for (int j = 0; j < parent.getChildren().size(); j++) { - entry = parent.getChildren().get(j); - entry.getAttachState().setStableIndex(sectionMemberIndex); - sectionMemberIndex++; + if (mFlags.isStabilityIndexFixEnabled()) { + entry.getAttachState().setStableIndex(sectionMemberIndex++); + if (entry instanceof GroupEntry) { + final GroupEntry parent = (GroupEntry) entry; + final NotificationEntry summary = parent.getSummary(); + if (summary != null) { + summary.getAttachState().setStableIndex(sectionMemberIndex++); + } + for (NotificationEntry child : parent.getChildren()) { + child.getAttachState().setStableIndex(sectionMemberIndex++); + } + } + } else { + // This old implementation uses the same index number for the group as the first + // child, and fails to assign an index to the summary. Remove once tested. + entry.getAttachState().setStableIndex(sectionMemberIndex); + if (entry instanceof GroupEntry) { + final GroupEntry parent = (GroupEntry) entry; + for (NotificationEntry child : parent.getChildren()) { + child.getAttachState().setStableIndex(sectionMemberIndex++); + } } + sectionMemberIndex++; } - sectionMemberIndex++; } } @@ -1196,7 +1263,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { o2.getSectionIndex()); if (cmp != 0) return cmp; - cmp = Integer.compare( + cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare( getStableOrderIndex(o1), getStableOrderIndex(o2)); if (cmp != 0) return cmp; @@ -1225,7 +1292,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> { - int cmp = Integer.compare( + int cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare( getStableOrderIndex(o1), getStableOrderIndex(o2)); if (cmp != 0) return cmp; @@ -1256,9 +1323,25 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // let the stability manager constrain or allow reordering return -1; } + // NOTE(b/241229236): Can't use cleared section index until we fix stability fragility return entry.getPreviousAttachState().getStableIndex(); } + @Nullable + private Integer getStableOrderRank(ListEntry entry) { + if (getStabilityManager().isEntryReorderingAllowed(entry)) { + // let the stability manager constrain or allow reordering + return null; + } + if (entry.getAttachState().getSectionIndex() + != entry.getPreviousAttachState().getSectionIndex()) { + // stable index is only valid within the same section; otherwise we allow reordering + return null; + } + final int stableIndex = entry.getPreviousAttachState().getStableIndex(); + return stableIndex == -1 ? null : stableIndex; + } + private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) { final NotifFilter filter = findRejectingFilter(entry, now, filters); entry.getAttachState().setExcludingFilter(filter); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt index 211e37473a70..68d1319699d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.coalescer -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject class GroupCoalescerLogger @Inject constructor( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt index e8f352f60da0..2919def16304 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.row.NotificationGuts import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index 8f3eb4f7e223..8a31ed9271ad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.app.Notification import android.app.Notification.GROUP_ALERT_SUMMARY import android.util.ArrayMap +import android.util.ArraySet +import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.notification.collection.GroupEntry @@ -70,6 +72,7 @@ class HeadsUpCoordinator @Inject constructor( @Main private val mExecutor: DelayableExecutor, ) : Coordinator { private val mEntriesBindingUntil = ArrayMap<String, Long>() + private val mEntriesUpdateTimes = ArrayMap<String, Long>() private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null private lateinit var mNotifPipeline: NotifPipeline private var mNow: Long = -1 @@ -264,6 +267,9 @@ class HeadsUpCoordinator @Inject constructor( } // After this method runs, all posted entries should have been handled (or skipped). mPostedEntries.clear() + + // Also take this opportunity to clean up any stale entry update times + cleanUpEntryUpdateTimes() } /** @@ -378,6 +384,9 @@ class HeadsUpCoordinator @Inject constructor( isAlerting = false, isBinding = false, ) + + // Record the last updated time for this key + setUpdateTime(entry, mSystemClock.currentTimeMillis()) } /** @@ -419,6 +428,9 @@ class HeadsUpCoordinator @Inject constructor( cancelHeadsUpBind(posted.entry) } } + + // Update last updated time for this entry + setUpdateTime(entry, mSystemClock.currentTimeMillis()) } /** @@ -426,6 +438,7 @@ class HeadsUpCoordinator @Inject constructor( */ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { mPostedEntries.remove(entry.key) + mEntriesUpdateTimes.remove(entry.key) cancelHeadsUpBind(entry) val entryKey = entry.key if (mHeadsUpManager.isAlerting(entryKey)) { @@ -454,7 +467,12 @@ class HeadsUpCoordinator @Inject constructor( // never) in mPostedEntries to need to alert, we need to check every notification // known to the pipeline. for (entry in mNotifPipeline.allNotifs) { - // The only entries we can consider alerting for here are entries that have never + // Only consider entries that are recent enough, since we want to apply a fairly + // strict threshold for when an entry should be updated via only ranking and not an + // app-provided notification update. + if (!isNewEnoughForRankingUpdate(entry)) continue + + // The only entries we consider alerting for here are entries that have never // interrupted and that now say they should heads up; if they've alerted in the // past, we don't want to incorrectly alert a second time if there wasn't an // explicit notification update. @@ -486,6 +504,41 @@ class HeadsUpCoordinator @Inject constructor( (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0) } + /** + * Sets the updated time for the given entry to the specified time. + */ + @VisibleForTesting + fun setUpdateTime(entry: NotificationEntry, time: Long) { + mEntriesUpdateTimes[entry.key] = time + } + + /** + * Checks whether the entry is new enough to be updated via ranking update. + * We want to avoid updating an entry too long after it was originally posted/updated when we're + * only reacting to a ranking change, as relevant ranking updates are expected to come in + * fairly soon after the posting of a notification. + */ + private fun isNewEnoughForRankingUpdate(entry: NotificationEntry): Boolean { + // If we don't have an update time for this key, default to "too old" + if (!mEntriesUpdateTimes.containsKey(entry.key)) return false + + val updateTime = mEntriesUpdateTimes[entry.key] ?: return false + return (mSystemClock.currentTimeMillis() - updateTime) <= MAX_RANKING_UPDATE_DELAY_MS + } + + private fun cleanUpEntryUpdateTimes() { + // Because we won't update entries that are older than this amount of time anyway, clean + // up any entries that are too old to notify. + val toRemove = ArraySet<String>() + for ((key, updateTime) in mEntriesUpdateTimes) { + if (updateTime == null || + (mSystemClock.currentTimeMillis() - updateTime) > MAX_RANKING_UPDATE_DELAY_MS) { + toRemove.add(key) + } + } + mEntriesUpdateTimes.removeAll(toRemove) + } + /** When an action is pressed on a notification, end HeadsUp lifetime extension. */ private val mActionPressListener = Consumer<NotificationEntry> { entry -> if (mNotifsExtendingLifetime.contains(entry)) { @@ -597,6 +650,9 @@ class HeadsUpCoordinator @Inject constructor( companion object { private const val TAG = "HeadsUpCoordinator" private const val BIND_TIMEOUT = 1000L + + // This value is set to match MAX_SOUND_DELAY_MS in NotificationRecord. + private const val MAX_RANKING_UPDATE_DELAY_MS: Long = 2000 } data class PostedEntry( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt index 8625cdbc89d5..dfaa291c6bb6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt @@ -1,9 +1,10 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.util.Log -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel + import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject private const val TAG = "HeadsUpCoordinator" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index 93146f9b3bf3..6e76691ae1b1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -407,10 +407,7 @@ public class PreparationCoordinator implements Coordinator { mLogger.logGroupInflationTookTooLong(group); return false; } - // Only delay release if the summary is not inflated. - // TODO(253454977): Once we ensure that all other pipeline filtering and pruning has been - // done by this point, we can revert back to checking for mInflatingNotifs.contains(...) - if (!isInflated(group.getSummary())) { + if (mInflatingNotifs.contains(group.getSummary())) { mLogger.logDelayingGroupRelease(group, group.getSummary()); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt index c4f4ed54e2fa..9558f47af795 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.coordinator -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt index c687e1bacbc9..d80445491bda 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.coordinator -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject private const val TAG = "ShadeEventCoordinator" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt new file mode 100644 index 000000000000..9ec8e07e73b3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.listbuilder + +import androidx.annotation.VisibleForTesting +import kotlin.math.sign + +class SemiStableSort { + val preallocatedWorkspace by lazy { ArrayList<Any>() } + val preallocatedAdditions by lazy { ArrayList<Any>() } + val preallocatedMapToIndex by lazy { HashMap<Any, Int>() } + val preallocatedMapToIndexComparator: Comparator<Any> by lazy { + Comparator.comparingInt { item -> preallocatedMapToIndex[item] ?: -1 } + } + + /** + * Sort the given [items] such that items which have a [stableOrder] will all be in that order, + * items without a [stableOrder] will be sorted according to the comparator, and the two sets of + * items will be combined to have the fewest elements out of order according to the [comparator] + * . The result will be placed into the original [items] list. + */ + fun <T : Any> sort( + items: MutableList<T>, + stableOrder: StableOrder<in T>, + comparator: Comparator<in T>, + ): Boolean = + withWorkspace<T, Boolean> { workspace -> + val ordered = + sortTo( + items, + stableOrder, + comparator, + workspace, + ) + items.clear() + items.addAll(workspace) + return ordered + } + + /** + * Sort the given [items] such that items which have a [stableOrder] will all be in that order, + * items without a [stableOrder] will be sorted according to the comparator, and the two sets of + * items will be combined to have the fewest elements out of order according to the [comparator] + * . The result will be put into [output]. + */ + fun <T : Any> sortTo( + items: Iterable<T>, + stableOrder: StableOrder<in T>, + comparator: Comparator<in T>, + output: MutableList<T>, + ): Boolean { + if (DEBUG) println("\n> START from ${items.map { it to stableOrder.getRank(it) }}") + // If array already has elements, use subList to ensure we only append + val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size) + items.filterTo(result) { stableOrder.getRank(it) != null } + result.sortBy { stableOrder.getRank(it)!! } + val isOrdered = result.isSorted(comparator) + withAdditions<T> { additions -> + items.filterTo(additions) { stableOrder.getRank(it) == null } + additions.sortWith(comparator) + insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator) + } + return isOrdered + } + + /** + * Rearrange the [sortedItems] to enforce that items are in the [stableOrder], and store the + * result in [output]. Items with a [stableOrder] will be in that order, items without a + * [stableOrder] will remain in same relative order as the input, and the two sets of items will + * be combined to have the fewest elements moved from their locations in the original. + */ + fun <T : Any> stabilizeTo( + sortedItems: Iterable<T>, + stableOrder: StableOrder<in T>, + output: MutableList<T>, + ): Boolean { + // Append to the output array if present + val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size) + sortedItems.filterTo(result) { stableOrder.getRank(it) != null } + val stableRankComparator = compareBy<T> { stableOrder.getRank(it)!! } + val isOrdered = result.isSorted(stableRankComparator) + if (!isOrdered) { + result.sortWith(stableRankComparator) + } + if (result.isEmpty()) { + sortedItems.filterTo(result) { stableOrder.getRank(it) == null } + return isOrdered + } + withAdditions<T> { additions -> + sortedItems.filterTo(additions) { stableOrder.getRank(it) == null } + if (additions.isNotEmpty()) { + withIndexOfComparator(sortedItems) { comparator -> + insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator) + } + } + } + return isOrdered + } + + private inline fun <T : Any, R> withWorkspace(block: (ArrayList<T>) -> R): R { + preallocatedWorkspace.clear() + val result = block(preallocatedWorkspace as ArrayList<T>) + preallocatedWorkspace.clear() + return result + } + + private inline fun <T : Any> withAdditions(block: (ArrayList<T>) -> Unit) { + preallocatedAdditions.clear() + block(preallocatedAdditions as ArrayList<T>) + preallocatedAdditions.clear() + } + + private inline fun <T : Any> withIndexOfComparator( + sortedItems: Iterable<T>, + block: (Comparator<in T>) -> Unit + ) { + preallocatedMapToIndex.clear() + sortedItems.forEachIndexed { i, item -> preallocatedMapToIndex[item] = i } + block(preallocatedMapToIndexComparator as Comparator<in T>) + preallocatedMapToIndex.clear() + } + + companion object { + + /** + * This is the core of the algorithm. + * + * Insert [preSortedAdditions] (the elements to be inserted) into [existing] without + * changing the relative order of any elements already in [existing], even though those + * elements may be mis-ordered relative to the [comparator], such that the total number of + * elements which are ordered incorrectly according to the [comparator] is fewest. + */ + private fun <T> insertPreSortedElementsWithFewestMisOrderings( + existing: MutableList<T>, + preSortedAdditions: Iterable<T>, + comparator: Comparator<in T>, + ) { + if (DEBUG) println(" To $existing insert $preSortedAdditions with fewest misordering") + var iStart = 0 + preSortedAdditions.forEach { toAdd -> + if (DEBUG) println(" need to add $toAdd to $existing, starting at $iStart") + var cmpSum = 0 + var cmpSumMax = 0 + var iCmpSumMax = iStart + if (DEBUG) print(" ") + for (i in iCmpSumMax until existing.size) { + val cmp = comparator.compare(toAdd, existing[i]).sign + cmpSum += cmp + if (cmpSum > cmpSumMax) { + cmpSumMax = cmpSum + iCmpSumMax = i + 1 + } + if (DEBUG) print("sum[$i]=$cmpSum, ") + } + if (DEBUG) println("inserting $toAdd at $iCmpSumMax") + existing.add(iCmpSumMax, toAdd) + iStart = iCmpSumMax + 1 + } + } + + /** Determines if a list is correctly sorted according to the given comparator */ + @VisibleForTesting + fun <T> List<T>.isSorted(comparator: Comparator<T>): Boolean { + if (this.size <= 1) { + return true + } + val iterator = this.iterator() + var previous = iterator.next() + var current: T? + while (iterator.hasNext()) { + current = iterator.next() + if (comparator.compare(previous, current) > 0) { + return false + } + previous = current + } + return true + } + } + + fun interface StableOrder<T> { + fun getRank(item: T): Int? + } +} + +val DEBUG = false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt new file mode 100644 index 000000000000..d8f75f61c05a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.listbuilder + +import com.android.systemui.statusbar.notification.collection.ListEntry + +object ShadeListBuilderHelper { + fun getSectionSubLists(entries: List<ListEntry>): Iterable<List<ListEntry>> = + getContiguousSubLists(entries, minLength = 1) { it.sectionIndex } + + inline fun <T : Any, K : Any> getContiguousSubLists( + itemList: List<T>, + minLength: Int = 1, + key: (T) -> K, + ): Iterable<List<T>> { + val subLists = mutableListOf<List<T>>() + val numEntries = itemList.size + var currentSectionStartIndex = 0 + var currentSectionKey: K? = null + for (i in 0 until numEntries) { + val sectionKey = key(itemList[i]) + if (currentSectionKey == null) { + currentSectionKey = sectionKey + } else if (currentSectionKey != sectionKey) { + val length = i - currentSectionStartIndex + if (length >= minLength) { + subLists.add(itemList.subList(currentSectionStartIndex, i)) + } + currentSectionStartIndex = i + currentSectionKey = sectionKey + } + } + val length = numEntries - currentSectionStartIndex + if (length >= minLength) { + subLists.add(itemList.subList(currentSectionStartIndex, numEntries)) + } + return subLists + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt index d8dae5d23f42..8e052c7dcc5d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt @@ -16,11 +16,11 @@ package com.android.systemui.statusbar.notification.collection.listbuilder -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.WARNING import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.WARNING import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt index aa27e1e407f0..911a2d0c2b36 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt @@ -20,13 +20,13 @@ import android.os.RemoteException import android.service.notification.NotificationListenerService import android.service.notification.NotificationListenerService.RankingMap import android.service.notification.StatusBarNotification -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.WARNING -import com.android.systemui.log.LogLevel.WTF import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.WARNING +import com.android.systemui.plugins.log.LogLevel.WTF import com.android.systemui.statusbar.notification.collection.NotifCollection import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason import com.android.systemui.statusbar.notification.collection.NotifCollection.FutureDismissal diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt index 38e3d496a60c..9c71e5c1054c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.render -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.util.Compile diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt index 6d1071c283e3..b4b9438cd6be 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.render -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import java.lang.RuntimeException import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt index 5dbec8dcba20..d4f11fc141f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.statusbar.notification.interruption -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt index 99d320d1c7ca..073b6b041b81 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt @@ -16,11 +16,11 @@ package com.android.systemui.statusbar.notification.interruption -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.WARNING import com.android.systemui.log.dagger.NotificationInterruptLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.WARNING import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt index fe03b2ad6a32..10197a38527e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.logging -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationRenderLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.notification.stack.NotificationSection diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt index ab91926d466a..46fef3f973a7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.row -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt index f9923b2254d7..8a5d29a1ae2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.row -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java index 2719dd88b7be..b2628e40e77e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java @@ -142,6 +142,11 @@ public class AmbientState implements Dumpable { */ private boolean mIsFlingRequiredAfterLockScreenSwipeUp = false; + /** + * Whether the shade is currently closing. + */ + private boolean mIsClosing; + @VisibleForTesting public boolean isFlingRequiredAfterLockScreenSwipeUp() { return mIsFlingRequiredAfterLockScreenSwipeUp; @@ -717,6 +722,20 @@ public class AmbientState implements Dumpable { && mStatusBarKeyguardViewManager.isBouncerInTransit(); } + /** + * @param isClosing Whether the shade is currently closing. + */ + public void setIsClosing(boolean isClosing) { + mIsClosing = isClosing; + } + + /** + * @return Whether the shade is currently closing. + */ + public boolean isClosing() { + return mIsClosing; + } + @Override public void dump(PrintWriter pw, String[] args) { pw.println("mTopPadding=" + mTopPadding); @@ -761,5 +780,6 @@ public class AmbientState implements Dumpable { + mIsFlingRequiredAfterLockScreenSwipeUp); pw.println("mZDistanceBetweenElements=" + mZDistanceBetweenElements); pw.println("mBaseZHeight=" + mBaseZHeight); + pw.println("mIsClosing=" + mIsClosing); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt index cb7dfe87f7fb..b61c55edadcd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt @@ -17,9 +17,9 @@ package com.android.systemui.statusbar.notification.stack import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationSectionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject private const val TAG = "NotifSections" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 55c577f1ea39..2272411b4314 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -255,7 +255,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private boolean mClearAllInProgress; private FooterClearAllListener mFooterClearAllListener; private boolean mFlingAfterUpEvent; - /** * Was the scroller scrolled to the top when the down motion was observed? */ @@ -4020,8 +4019,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable setOwnScrollY(0); } + @VisibleForTesting @ShadeViewRefactor(RefactorComponent.COORDINATOR) - private void setIsExpanded(boolean isExpanded) { + void setIsExpanded(boolean isExpanded) { boolean changed = isExpanded != mIsExpanded; mIsExpanded = isExpanded; mStackScrollAlgorithm.setIsExpanded(isExpanded); @@ -4842,13 +4842,21 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } } + @VisibleForTesting @ShadeViewRefactor(RefactorComponent.COORDINATOR) - private void setOwnScrollY(int ownScrollY) { + void setOwnScrollY(int ownScrollY) { setOwnScrollY(ownScrollY, false /* animateScrollChangeListener */); } @ShadeViewRefactor(RefactorComponent.COORDINATOR) private void setOwnScrollY(int ownScrollY, boolean animateStackYChangeListener) { + // Avoid Flicking during clear all + // when the shade finishes closing, onExpansionStopped will call + // resetScrollPosition to setOwnScrollY to 0 + if (mAmbientState.isClosing()) { + return; + } + if (ownScrollY != mOwnScrollY) { // We still want to call the normal scrolled changed for accessibility reasons onScrollChanged(mScrollX, ownScrollY, mScrollX, mOwnScrollY); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt index 5f79c0e3913a..4c52db7f8732 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.statusbar.notification.stack -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt index cb4a0884fea4..f5de678a8536 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.statusbar.notification.stack -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 25fd483efa2d..3d5e37376fa9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -455,6 +455,9 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn void collapseShade(); + /** Collapse the shade, but conditional on a flag specific to the trigger of a bugreport. */ + void collapseShadeForBugreport(); + int getWakefulnessState(); boolean isScreenFullyOff(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 2c834cf781a6..709a434c08ac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -868,6 +868,11 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mBubblesOptional.get().setExpandListener(mBubbleExpandListener); } + // Do not restart System UI when the bugreport flag changes. + mFeatureFlags.addListener(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, event -> { + event.requestNoRestart(); + }); + mStatusBarSignalPolicy.init(); mKeyguardIndicationController.init(); @@ -3561,6 +3566,13 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } } + @Override + public void collapseShadeForBugreport() { + if (!mFeatureFlags.isEnabled(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT)) { + collapseShade(); + } + } + @VisibleForTesting final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt index 02b235493715..4839fe6a7bef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt @@ -19,9 +19,9 @@ package com.android.systemui.statusbar.phone import android.util.DisplayMetrics import android.view.View import com.android.internal.logging.nano.MetricsProto.MetricsEvent -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.LSShadeTransitionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java index 00c3e8fac0b4..5e2a7c8ca540 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java @@ -26,6 +26,7 @@ import android.view.ViewGroup; import com.android.systemui.R; import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.animation.Expandable; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.plugins.ActivityStarter; @@ -67,7 +68,7 @@ public class MultiUserSwitchController extends ViewController<MultiUserSwitch> { ActivityLaunchAnimator.Controller.fromView(v, null), true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM); } else { - mUserSwitchDialogController.showDialog(v); + mUserSwitchDialogController.showDialog(v.getContext(), Expandable.fromView(v)); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 5f5ec68ba898..30591d08075a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -1094,7 +1094,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mCentralSurfaces.endAffordanceLaunch(); // The second condition is for SIM card locked bouncer - if (bouncerIsScrimmed() && needsFullscreenBouncer()) { + if (bouncerIsScrimmed() && !needsFullscreenBouncer()) { hideBouncer(false); updateStates(); } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt index b9a1413ff791..81edff45c505 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt @@ -17,12 +17,12 @@ package com.android.systemui.statusbar.phone import android.app.PendingIntent -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.WARNING import com.android.systemui.log.dagger.NotifInteractionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.WARNING import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt index 28ed0806a181..d64bc58a0c37 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.phone.fragment -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.CollapsedSbFragmentLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.disableflags.DisableFlagsLogger import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt index 0d52f46e571f..e498ae451400 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.phone.userswitcher import android.content.Intent import android.os.UserHandle import android.view.View +import com.android.systemui.animation.Expandable import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.plugins.ActivityStarter @@ -75,7 +76,7 @@ class StatusBarUserSwitcherControllerImpl @Inject constructor( null /* ActivityLaunchAnimator.Controller */, true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM) } else { - userSwitcherDialogController.showDialog(view) + userSwitcherDialogController.showDialog(view.context, Expandable.fromView(view)) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt index dbb1aa54d8ee..d3cf32fb44ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt @@ -18,10 +18,10 @@ package com.android.systemui.statusbar.pipeline.shared import android.net.Network import android.net.NetworkCapabilities -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.StatusBarConnectivityLog +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString import javax.inject.Inject import kotlinx.coroutines.flow.Flow diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt index 2f0ebf752a23..28a9b97b8ea6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt @@ -43,11 +43,7 @@ protected constructor( } override fun getCount(): Int { - return if (controller.isKeyguardShowing) { - users.count { !it.isRestricted } - } else { - users.size - } + return users.size } override fun getItem(position: Int): UserRecord { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt index d7c81af53d8b..df1e80b78c9b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt @@ -16,10 +16,10 @@ package com.android.systemui.statusbar.policy -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.VERBOSE import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.VERBOSE import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java index dc73d1f007c6..f63d65246d9b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java @@ -36,6 +36,7 @@ import com.android.keyguard.KeyguardVisibilityHelper; import com.android.keyguard.dagger.KeyguardUserSwitcherScope; import com.android.settingslib.drawable.CircleFramedDrawable; import com.android.systemui.R; +import com.android.systemui.animation.Expandable; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -190,7 +191,8 @@ public class KeyguardQsUserSwitchController extends ViewController<FrameLayout> mUiEventLogger.log( LockscreenGestureLogger.LockscreenUiEvent.LOCKSCREEN_SWITCH_USER_TAP); - mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground); + mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground.getContext(), + Expandable.fromView(mUserAvatarViewWithBackground)); }); mUserAvatarView.setAccessibilityDelegate(new View.AccessibilityDelegate() { diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt index 606a11a84686..a7185cb18c40 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt @@ -16,8 +16,8 @@ package com.android.systemui.temporarydisplay -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel /** A logger for temporary view changes -- see [TemporaryViewDisplayController]. */ open class TemporaryViewLogger( diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt index 1a25e4df3a61..45ce687a1a4d 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt @@ -18,7 +18,6 @@ package com.android.systemui.temporarydisplay.chipbar import android.content.Context import android.graphics.Rect -import android.media.MediaRoute2Info import android.os.PowerManager import android.view.Gravity import android.view.MotionEvent @@ -27,25 +26,24 @@ import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.widget.TextView -import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.internal.widget.CachingIconView import com.android.systemui.Gefingerpoken import com.android.systemui.R import com.android.systemui.animation.Interpolators import com.android.systemui.animation.ViewHierarchyAnimator import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Text.Companion.loadText +import com.android.systemui.common.ui.binder.IconViewBinder +import com.android.systemui.common.ui.binder.TextViewBinder import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.common.MediaTttUtils -import com.android.systemui.media.taptotransfer.sender.ChipStateSender import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger -import com.android.systemui.media.taptotransfer.sender.TransferStatus import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.temporarydisplay.TemporaryViewDisplayController -import com.android.systemui.temporarydisplay.TemporaryViewInfo import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.view.ViewUtil import javax.inject.Inject @@ -78,11 +76,10 @@ open class ChipbarCoordinator @Inject constructor( accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, powerManager: PowerManager, - private val uiEventLogger: MediaTttSenderUiEventLogger, private val falsingManager: FalsingManager, private val falsingCollector: FalsingCollector, private val viewUtil: ViewUtil, -) : TemporaryViewDisplayController<ChipSenderInfo, MediaTttLogger>( +) : TemporaryViewDisplayController<ChipbarInfo, MediaTttLogger>( context, logger, windowManager, @@ -104,15 +101,13 @@ open class ChipbarCoordinator @Inject constructor( override fun start() {} override fun updateView( - newInfo: ChipSenderInfo, + newInfo: ChipbarInfo, currentView: ViewGroup ) { // TODO(b/245610654): Adding logging here. - val chipState = newInfo.state - // Detect falsing touches on the chip. - parent = currentView.requireViewById(R.id.media_ttt_sender_chip) + parent = currentView.requireViewById(R.id.chipbar_root_view) parent.touchHandler = object : Gefingerpoken { override fun onTouchEvent(ev: MotionEvent?): Boolean { falsingCollector.onTouchEvent(ev) @@ -120,47 +115,49 @@ open class ChipbarCoordinator @Inject constructor( } } - // App icon - val iconInfo = MediaTttUtils.getIconInfoFromPackageName( - context, newInfo.routeInfo.clientPackageName, logger - ) - val iconView = currentView.requireViewById<CachingIconView>(R.id.app_icon) - iconView.setImageDrawable(iconInfo.drawable) - iconView.contentDescription = iconInfo.contentDescription + // ---- Start icon ---- + val iconView = currentView.requireViewById<CachingIconView>(R.id.start_icon) + IconViewBinder.bind(newInfo.startIcon, iconView) - // Text - val otherDeviceName = newInfo.routeInfo.name.toString() - val chipText = chipState.getChipTextString(context, otherDeviceName) - currentView.requireViewById<TextView>(R.id.text).text = chipText + // ---- Text ---- + val textView = currentView.requireViewById<TextView>(R.id.text) + TextViewBinder.bind(textView, newInfo.text) + // ---- End item ---- // Loading currentView.requireViewById<View>(R.id.loading).visibility = - (chipState.transferStatus == TransferStatus.IN_PROGRESS).visibleIfTrue() - - // Undo - val undoView = currentView.requireViewById<View>(R.id.undo) - val undoClickListener = chipState.undoClickListener( - this, - newInfo.routeInfo, - newInfo.undoCallback, - uiEventLogger, - falsingManager, - ) - undoView.setOnClickListener(undoClickListener) - undoView.visibility = (undoClickListener != null).visibleIfTrue() + (newInfo.endItem == ChipbarEndItem.Loading).visibleIfTrue() - // Failure - currentView.requireViewById<View>(R.id.failure_icon).visibility = - (chipState.transferStatus == TransferStatus.FAILED).visibleIfTrue() + // Error + currentView.requireViewById<View>(R.id.error).visibility = + (newInfo.endItem == ChipbarEndItem.Error).visibleIfTrue() + + // Button + val buttonView = currentView.requireViewById<TextView>(R.id.end_button) + if (newInfo.endItem is ChipbarEndItem.Button) { + TextViewBinder.bind(buttonView, newInfo.endItem.text) + + val onClickListener = View.OnClickListener { clickedView -> + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener + newInfo.endItem.onClickListener.onClick(clickedView) + } - // For accessibility + buttonView.setOnClickListener(onClickListener) + buttonView.visibility = View.VISIBLE + } else { + buttonView.visibility = View.GONE + } + + // ---- Overall accessibility ---- currentView.requireViewById<ViewGroup>( - R.id.media_ttt_sender_chip_inner - ).contentDescription = "${iconInfo.contentDescription} $chipText" + R.id.chipbar_inner + ).contentDescription = + "${newInfo.startIcon.contentDescription.loadContentDescription(context)} " + + "${newInfo.text.loadText(context)}" } override fun animateViewIn(view: ViewGroup) { - val chipInnerView = view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner) + val chipInnerView = view.requireViewById<ViewGroup>(R.id.chipbar_inner) ViewHierarchyAnimator.animateAddition( chipInnerView, ViewHierarchyAnimator.Hotspot.TOP, @@ -175,7 +172,7 @@ open class ChipbarCoordinator @Inject constructor( override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { ViewHierarchyAnimator.animateRemoval( - view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner), + view.requireViewById<ViewGroup>(R.id.chipbar_inner), ViewHierarchyAnimator.Hotspot.TOP, Interpolators.EMPHASIZED_ACCELERATE, ANIMATION_DURATION, @@ -197,13 +194,5 @@ open class ChipbarCoordinator @Inject constructor( } } -data class ChipSenderInfo( - val state: ChipStateSender, - val routeInfo: MediaRoute2Info, - val undoCallback: IUndoMediaTransferCallback? = null -) : TemporaryViewInfo { - override fun getTimeoutMs() = state.timeout -} - const val SENDER_TAG = "MediaTapToTransferSender" private const val ANIMATION_DURATION = 500L diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt new file mode 100644 index 000000000000..211a66387966 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.temporarydisplay.chipbar + +import android.view.View +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text +import com.android.systemui.temporarydisplay.TemporaryViewInfo + +/** + * A container for all the state needed to display a chipbar via [ChipbarCoordinator]. + * + * @property startIcon the icon to display at the start of the chipbar (on the left in LTR locales; + * on the right in RTL locales). + * @property text the text to display. + * @property endItem an optional end item to display at the end of the chipbar (on the right in LTR + * locales; on the left in RTL locales). + */ +data class ChipbarInfo( + val startIcon: Icon, + val text: Text, + val endItem: ChipbarEndItem?, +) : TemporaryViewInfo + +/** The possible items to display at the end of the chipbar. */ +sealed class ChipbarEndItem { + /** A loading icon should be displayed. */ + object Loading : ChipbarEndItem() + + /** An error icon should be displayed. */ + object Error : ChipbarEndItem() + + /** + * A button with the provided [text] and [onClickListener] functionality should be displayed. + */ + data class Button(val text: Text, val onClickListener: View.OnClickListener) : ChipbarEndItem() + + // TODO(b/245610654): Add support for a generic icon. +} diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 3d56f2317660..3ecb15b9d79c 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -79,6 +79,7 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; @@ -114,6 +115,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { private final SecureSettings mSecureSettings; private final Executor mMainExecutor; private final Handler mBgHandler; + private final boolean mIsMonochromaticEnabled; private final Context mContext; private final boolean mIsMonetEnabled; private final UserTracker mUserTracker; @@ -363,6 +365,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags, @Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle) { mContext = context; + mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEMES); mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET); mDeviceProvisionedController = deviceProvisionedController; mBroadcastDispatcher = broadcastDispatcher; @@ -665,8 +668,13 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { // Allow-list of Style objects that can be created from a setting string, i.e. can be // used as a system-wide theme. // - Content intentionally excluded, intended for media player, not system-wide - List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT, - Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT); + List<Style> validStyles = new ArrayList<>(Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, + Style.TONAL_SPOT, Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT)); + + if (mIsMonochromaticEnabled) { + validStyles.add(Style.MONOCHROMATIC); + } + Style style = mThemeStyle; final String overlayPackageJson = mSecureSettings.getStringForUser( Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt index 51541bd3032e..fda511433143 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt @@ -16,11 +16,11 @@ package com.android.systemui.toast -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.ToastLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogMessage import javax.inject.Inject private const val TAG = "ToastLog" diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index 919e699652bc..d768b6dc195a 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -321,6 +321,7 @@ constructor( return when { isAddUser -> false isAddSupervisedUser -> false + isManageUsers -> false isGuest -> info != null else -> true } @@ -346,6 +347,7 @@ constructor( isAddUser -> UserActionModel.ADD_USER isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER isGuest -> UserActionModel.ENTER_GUEST_MODE + isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT else -> error("Don't know how to convert to UserActionModel: $this") } } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index ba5a82a42d94..0d5c64b83e6e 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -236,18 +236,7 @@ constructor( } .flatMapLatest { isActionable -> if (isActionable) { - repository.actions.map { actions -> - actions + - if (actions.isNotEmpty()) { - // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT - // because that's a user switcher specific action that is - // not known to the our data source or other features. - listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - } else { - // If no actions, don't add the navigate action. - emptyList() - } - } + repository.actions } else { // If not actionable it means that we're not allowed to show actions // when diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt index 91c592177d19..f7e19c0ca810 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -30,6 +30,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.plugins.FalsingManager import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.domain.model.ShowDialogRequestModel +import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect @@ -41,19 +42,19 @@ import kotlinx.coroutines.launch class UserSwitcherDialogCoordinator @Inject constructor( - @Application private val context: Context, - @Application private val applicationScope: CoroutineScope, - private val falsingManager: FalsingManager, - private val broadcastSender: BroadcastSender, - private val dialogLaunchAnimator: DialogLaunchAnimator, - private val interactor: UserInteractor, - private val featureFlags: FeatureFlags, + @Application private val context: Lazy<Context>, + @Application private val applicationScope: Lazy<CoroutineScope>, + private val falsingManager: Lazy<FalsingManager>, + private val broadcastSender: Lazy<BroadcastSender>, + private val dialogLaunchAnimator: Lazy<DialogLaunchAnimator>, + private val interactor: Lazy<UserInteractor>, + private val featureFlags: Lazy<FeatureFlags>, ) : CoreStartable { private var currentDialog: Dialog? = null override fun start() { - if (featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) { + if (featureFlags.get().isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) { return } @@ -62,8 +63,8 @@ constructor( } private fun startHandlingDialogShowRequests() { - applicationScope.launch { - interactor.dialogShowRequests.filterNotNull().collect { request -> + applicationScope.get().launch { + interactor.get().dialogShowRequests.filterNotNull().collect { request -> currentDialog?.let { if (it.isShowing) { it.cancel() @@ -74,48 +75,48 @@ constructor( when (request) { is ShowDialogRequestModel.ShowAddUserDialog -> AddUserDialog( - context = context, + context = context.get(), userHandle = request.userHandle, isKeyguardShowing = request.isKeyguardShowing, showEphemeralMessage = request.showEphemeralMessage, - falsingManager = falsingManager, - broadcastSender = broadcastSender, - dialogLaunchAnimator = dialogLaunchAnimator, + falsingManager = falsingManager.get(), + broadcastSender = broadcastSender.get(), + dialogLaunchAnimator = dialogLaunchAnimator.get(), ) is ShowDialogRequestModel.ShowUserCreationDialog -> UserCreatingDialog( - context, + context.get(), request.isGuest, ) is ShowDialogRequestModel.ShowExitGuestDialog -> ExitGuestDialog( - context = context, + context = context.get(), guestUserId = request.guestUserId, isGuestEphemeral = request.isGuestEphemeral, targetUserId = request.targetUserId, isKeyguardShowing = request.isKeyguardShowing, - falsingManager = falsingManager, - dialogLaunchAnimator = dialogLaunchAnimator, + falsingManager = falsingManager.get(), + dialogLaunchAnimator = dialogLaunchAnimator.get(), onExitGuestUserListener = request.onExitGuestUser, ) } currentDialog?.show() - interactor.onDialogShown() + interactor.get().onDialogShown() } } } private fun startHandlingDialogDismissRequests() { - applicationScope.launch { - interactor.dialogDismissRequests.filterNotNull().collect { + applicationScope.get().launch { + interactor.get().dialogDismissRequests.filterNotNull().collect { currentDialog?.let { if (it.isShowing) { it.cancel() } } - interactor.onDialogDismissed() + interactor.get().onDialogDismissed() } } } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt index 219dae29117f..d857e85bac53 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt @@ -62,17 +62,7 @@ private constructor( val isMenuVisible: Flow<Boolean> = _isMenuVisible /** The user action menu. */ val menu: Flow<List<UserActionViewModel>> = - userInteractor.actions.map { actions -> - if (isNewImpl && actions.isNotEmpty()) { - // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user - // switcher specific action that is not known to the our data source or other - // features. - actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - } else { - actions - } - .map { action -> toViewModel(action) } - } + userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } } /** Whether the button to open the user action menu is visible. */ val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() } diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java index ecb365f43e3f..2c317dd391c0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java +++ b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java @@ -172,10 +172,14 @@ public abstract class Condition implements CallbackController<Condition.Callback return Boolean.TRUE.equals(mIsConditionMet); } - private boolean shouldLog() { + protected final boolean shouldLog() { return Log.isLoggable(mTag, Log.DEBUG); } + protected final String getTag() { + return mTag; + } + /** * Callback that receives updates about whether the condition has been fulfilled. */ diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java index 4824f6744c6e..cb430ba454f0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java +++ b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java @@ -117,6 +117,7 @@ public class Monitor { final SubscriptionState state = new SubscriptionState(subscription); mExecutor.execute(() -> { + if (shouldLog()) Log.d(mTag, "adding subscription"); mSubscriptions.put(token, state); // Add and associate conditions. @@ -143,7 +144,7 @@ public class Monitor { */ public void removeSubscription(@NotNull Subscription.Token token) { mExecutor.execute(() -> { - if (shouldLog()) Log.d(mTag, "removing callback"); + if (shouldLog()) Log.d(mTag, "removing subscription"); if (!mSubscriptions.containsKey(token)) { Log.e(mTag, "subscription not present:" + token); return; diff --git a/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt new file mode 100644 index 000000000000..9d6aff219148 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2020 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.keyguard + +import android.content.Context +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.util.AttributeSet +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class BouncerKeyguardMessageAreaTest : SysuiTestCase() { + class FakeBouncerKeyguardMessageArea(context: Context, attrs: AttributeSet?) : + BouncerKeyguardMessageArea(context, attrs) { + override val SHOW_DURATION_MILLIS = 0L + override val HIDE_DURATION_MILLIS = 0L + } + lateinit var underTest: BouncerKeyguardMessageArea + + @Before + fun setup() { + underTest = FakeBouncerKeyguardMessageArea(context, null) + } + + @Test + fun testSetSameMessage() { + val underTestSpy = spy(underTest) + underTestSpy.setMessage("abc") + underTestSpy.setMessage("abc") + verify(underTestSpy, times(1)).text = "abc" + } + + @Test + fun testSetDifferentMessage() { + underTest.setMessage("abc") + underTest.setMessage("def") + assertThat(underTest.text).isEqualTo("def") + } + + @Test + fun testSetNullMessage() { + underTest.setMessage(null) + assertThat(underTest.text).isEqualTo("") + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 8a2c35410586..03efd06d0e5e 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -17,17 +17,21 @@ package com.android.keyguard import android.content.BroadcastReceiver import android.testing.AndroidTestingRunner +import android.view.View import android.widget.TextView import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.flags.FeatureFlags +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.plugins.ClockAnimations import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockEvents import com.android.systemui.plugins.ClockFaceController import com.android.systemui.plugins.ClockFaceEvents -import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.mockito.any @@ -37,6 +41,9 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import java.util.TimeZone import java.util.concurrent.Executor +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule @@ -57,7 +64,7 @@ import org.mockito.junit.MockitoJUnit class ClockEventControllerTest : SysuiTestCase() { @JvmField @Rule val mockito = MockitoJUnit.rule() - @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var keyguardInteractor: KeyguardInteractor @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var batteryController: BatteryController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @@ -72,8 +79,11 @@ class ClockEventControllerTest : SysuiTestCase() { @Mock private lateinit var largeClockController: ClockFaceController @Mock private lateinit var smallClockEvents: ClockFaceEvents @Mock private lateinit var largeClockEvents: ClockFaceEvents + @Mock private lateinit var parentView: View + @Mock private lateinit var transitionRepository: KeyguardTransitionRepository + private lateinit var repository: FakeKeyguardRepository - private lateinit var clockEventController: ClockEventController + private lateinit var underTest: ClockEventController @Before fun setUp() { @@ -86,8 +96,11 @@ class ClockEventControllerTest : SysuiTestCase() { whenever(clock.events).thenReturn(events) whenever(clock.animations).thenReturn(animations) - clockEventController = ClockEventController( - statusBarStateController, + repository = FakeKeyguardRepository() + + underTest = ClockEventController( + KeyguardInteractor(repository = repository), + KeyguardTransitionInteractor(repository = transitionRepository), broadcastDispatcher, batteryController, keyguardUpdateMonitor, @@ -98,31 +111,33 @@ class ClockEventControllerTest : SysuiTestCase() { bgExecutor, featureFlags ) + underTest.clock = clock + + runBlocking(IMMEDIATE) { + underTest.registerListeners(parentView) + + repository.setDozing(true) + repository.setDozeAmount(1f) + } } @Test fun clockSet_validateInitialization() { - clockEventController.clock = clock - verify(clock).initialize(any(), anyFloat(), anyFloat()) } @Test fun clockUnset_validateState() { - clockEventController.clock = clock - clockEventController.clock = null + underTest.clock = null - assertEquals(clockEventController.clock, null) + assertEquals(underTest.clock, null) } @Test - fun themeChanged_verifyClockPaletteUpdated() { - clockEventController.clock = clock + fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) { verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) - clockEventController.registerListeners() - val captor = argumentCaptor<ConfigurationController.ConfigurationListener>() verify(configurationController).addCallback(capture(captor)) captor.value.onThemeChanged() @@ -131,13 +146,10 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun fontChanged_verifyFontSizeUpdated() { - clockEventController.clock = clock + fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) { verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) - clockEventController.registerListeners() - val captor = argumentCaptor<ConfigurationController.ConfigurationListener>() verify(configurationController).addCallback(capture(captor)) captor.value.onDensityOrFontScaleChanged() @@ -146,10 +158,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) { val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() verify(batteryController).addCallback(capture(batteryCaptor)) val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() @@ -161,26 +170,21 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - - val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() - verify(batteryController).addCallback(capture(batteryCaptor)) - val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() - verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) - keyguardCaptor.value.onKeyguardVisibilityChanged(true) - batteryCaptor.value.onBatteryLevelChanged(10, false, true) - batteryCaptor.value.onBatteryLevelChanged(10, false, true) - - verify(animations, times(1)).charge() - } + fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() = + runBlocking(IMMEDIATE) { + val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() + verify(batteryController).addCallback(capture(batteryCaptor)) + val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) + keyguardCaptor.value.onKeyguardVisibilityChanged(true) + batteryCaptor.value.onBatteryLevelChanged(10, false, true) + batteryCaptor.value.onBatteryLevelChanged(10, false, true) + + verify(animations, times(1)).charge() + } @Test - fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) { val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() verify(batteryController).addCallback(capture(batteryCaptor)) val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() @@ -192,25 +196,20 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - - val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() - verify(batteryController).addCallback(capture(batteryCaptor)) - val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() - verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) - keyguardCaptor.value.onKeyguardVisibilityChanged(true) - batteryCaptor.value.onBatteryLevelChanged(10, false, false) - - verify(animations, never()).charge() - } + fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() = + runBlocking(IMMEDIATE) { + val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() + verify(batteryController).addCallback(capture(batteryCaptor)) + val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) + keyguardCaptor.value.onKeyguardVisibilityChanged(true) + batteryCaptor.value.onBatteryLevelChanged(10, false, false) + + verify(animations, never()).charge() + } @Test - fun localeCallback_verifyClockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun localeCallback_verifyClockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<BroadcastReceiver>() verify(broadcastDispatcher).registerReceiver( capture(captor), any(), eq(null), eq(null), anyInt(), eq(null) @@ -221,10 +220,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_visibilityChanged_clockDozeCalled() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_visibilityChanged_clockDozeCalled() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) @@ -236,10 +232,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_timeFormat_clockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_timeFormat_clockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onTimeFormatChanged("12h") @@ -248,11 +241,8 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_timezoneChanged_clockNotified() { + fun keyguardCallback_timezoneChanged_clockNotified() = runBlocking(IMMEDIATE) { val mockTimeZone = mock<TimeZone>() - clockEventController.clock = clock - clockEventController.registerListeners() - val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onTimeZoneChanged(mockTimeZone) @@ -261,10 +251,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_userSwitched_clockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_userSwitched_clockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onUserSwitchComplete(10) @@ -273,25 +260,27 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_verifyKeyguardChanged() { - clockEventController.clock = clock - clockEventController.registerListeners() + fun keyguardCallback_verifyKeyguardChanged() = runBlocking(IMMEDIATE) { + val job = underTest.listenForDozeAmount(this) + repository.setDozeAmount(0.4f) - val captor = argumentCaptor<StatusBarStateController.StateListener>() - verify(statusBarStateController).addCallback(capture(captor)) - captor.value.onDozeAmountChanged(0.4f, 0.6f) + yield() verify(animations).doze(0.4f) + + job.cancel() } @Test - fun unregisterListeners_validate() { - clockEventController.clock = clock - clockEventController.unregisterListeners() + fun unregisterListeners_validate() = runBlocking(IMMEDIATE) { + underTest.unregisterListeners() verify(broadcastDispatcher).unregisterReceiver(any()) verify(configurationController).removeCallback(any()) verify(batteryController).removeCallback(any()) verify(keyguardUpdateMonitor).removeCallback(any()) - verify(statusBarStateController).removeCallback(any()) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index 9b2bba612106..627d738a895f 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -280,6 +280,6 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { private void verifyAttachment(VerificationMode times) { verify(mClockRegistry, times).registerClockChangeListener( any(ClockRegistry.ClockChangeListener.class)); - verify(mClockEventController, times).registerListeners(); + verify(mClockEventController, times).registerListeners(mView); } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java index 69524e5a4537..5d2b0ca4e7ea 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java @@ -17,13 +17,11 @@ package com.android.keyguard; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; -import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; @@ -92,19 +90,4 @@ public class KeyguardMessageAreaControllerTest extends SysuiTestCase { mMessageAreaController.setIsVisible(true); verify(mKeyguardMessageArea).setIsVisible(true); } - - @Test - public void testSetMessageIfEmpty_empty() { - mMessageAreaController.setMessage(""); - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin); - verify(mKeyguardMessageArea).setMessage(R.string.keyguard_enter_your_pin); - } - - @Test - public void testSetMessageIfEmpty_notEmpty() { - mMessageAreaController.setMessage("abc"); - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin); - verify(mKeyguardMessageArea, never()).setMessage(getContext() - .getResources().getText(R.string.keyguard_enter_your_pin)); - } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt index b89dbd98968a..b369098cafc0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt @@ -114,9 +114,8 @@ class KeyguardPasswordViewControllerTest : SysuiTestCase() { } @Test - fun onResume_testSetInitialText() { - keyguardPasswordViewController.onResume(KeyguardSecurityView.SCREEN_ON) - verify(mKeyguardMessageAreaController) - .setMessageIfEmpty(R.string.keyguard_enter_your_password) + fun startAppearAnimation() { + keyguardPasswordViewController.startAppearAnimation() + verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password) } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt index 3262a77b7711..9eff70487c74 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt @@ -100,16 +100,16 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { } @Test - fun onPause_clearsTextField() { + fun onPause_resetsText() { mKeyguardPatternViewController.init() mKeyguardPatternViewController.onPause() - verify(mKeyguardMessageAreaController).setMessage("") + verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern) } + @Test - fun onResume_setInitialText() { - mKeyguardPatternViewController.onResume(KeyguardSecurityView.SCREEN_ON) - verify(mKeyguardMessageAreaController) - .setMessageIfEmpty(R.string.keyguard_enter_your_pattern) + fun startAppearAnimation() { + mKeyguardPatternViewController.startAppearAnimation() + verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern) } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java index 97d556b04aa4..ce1101f389c0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java @@ -113,11 +113,4 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON); verify(mPasswordEntry).requestFocus(); } - - @Test - public void onResume_setInitialText() { - mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON); - verify(mKeyguardMessageAreaController).setMessageIfEmpty(R.string.keyguard_enter_your_pin); - } } - diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index 9e5bfe53ea05..d9efdeaea04c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -98,6 +98,6 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { @Test fun startAppearAnimation() { pinViewController.startAppearAnimation() - verify(keyguardMessageAreaController).setMessageIfEmpty(R.string.keyguard_enter_your_pin) + verify(keyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index baeabc577fb7..cd50144bf2e8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -163,6 +163,7 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { val sensorBounds = Rect(0, 0, SENSOR_WIDTH, SENSOR_HEIGHT) overlayParams = UdfpsOverlayParams( sensorBounds, + sensorBounds, DISPLAY_WIDTH, DISPLAY_HEIGHT, scaleFactor = 1f, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index f210708806ab..eff47bd2ee98 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -414,7 +414,7 @@ public class UdfpsControllerTest extends SysuiTestCase { final float[] scaleFactor = new float[]{1f, displayHeight[1] / (float) displayHeight[0]}; final int[] rotation = new int[]{Surface.ROTATION_0, Surface.ROTATION_90}; final UdfpsOverlayParams oldParams = new UdfpsOverlayParams(sensorBounds[0], - displayWidth[0], displayHeight[0], scaleFactor[0], rotation[0]); + sensorBounds[0], displayWidth[0], displayHeight[0], scaleFactor[0], rotation[0]); for (int i1 = 0; i1 <= 1; ++i1) { for (int i2 = 0; i2 <= 1; ++i2) { @@ -422,8 +422,8 @@ public class UdfpsControllerTest extends SysuiTestCase { for (int i4 = 0; i4 <= 1; ++i4) { for (int i5 = 0; i5 <= 1; ++i5) { final UdfpsOverlayParams newParams = new UdfpsOverlayParams( - sensorBounds[i1], displayWidth[i2], displayHeight[i3], - scaleFactor[i4], rotation[i5]); + sensorBounds[i1], sensorBounds[i1], displayWidth[i2], + displayHeight[i3], scaleFactor[i4], rotation[i5]); if (newParams.equals(oldParams)) { continue; @@ -466,8 +466,8 @@ public class UdfpsControllerTest extends SysuiTestCase { // Initialize the overlay. mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, - new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor, - rotation)); + new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, + scaleFactor, rotation)); // Show the overlay. mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID, @@ -477,8 +477,8 @@ public class UdfpsControllerTest extends SysuiTestCase { // Update overlay with the same parameters. mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, - new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor, - rotation)); + new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, + scaleFactor, rotation)); mFgExecutor.runAllReady(); // Ensure the overlay was not recreated. @@ -525,8 +525,8 @@ public class UdfpsControllerTest extends SysuiTestCase { // Test ROTATION_0 mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, - new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor, - Surface.ROTATION_0)); + new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, + scaleFactor, Surface.ROTATION_0)); MotionEvent event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); @@ -542,8 +542,8 @@ public class UdfpsControllerTest extends SysuiTestCase { // Test ROTATION_90 reset(mAlternateTouchProvider); mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, - new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor, - Surface.ROTATION_90)); + new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, + scaleFactor, Surface.ROTATION_90)); event = obtainMotionEvent(ACTION_DOWN, displayHeight, 0, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricsExecutor.runAllReady(); @@ -558,8 +558,8 @@ public class UdfpsControllerTest extends SysuiTestCase { // Test ROTATION_270 reset(mAlternateTouchProvider); mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, - new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor, - Surface.ROTATION_270)); + new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, + scaleFactor, Surface.ROTATION_270)); event = obtainMotionEvent(ACTION_DOWN, 0, displayWidth, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricsExecutor.runAllReady(); @@ -574,8 +574,8 @@ public class UdfpsControllerTest extends SysuiTestCase { // Test ROTATION_180 reset(mAlternateTouchProvider); mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, - new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor, - Surface.ROTATION_180)); + new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, + scaleFactor, Surface.ROTATION_180)); // ROTATION_180 is not supported. It should be treated like ROTATION_0. event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt index b78c06391057..ac936e1a77c5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt @@ -68,7 +68,8 @@ class UdfpsViewTest : SysuiTestCase() { view = LayoutInflater.from(context).inflate(R.layout.udfps_view, null) as UdfpsView view.animationViewController = animationViewController val sensorBounds = SensorLocationInternal("", SENSOR_X, SENSOR_Y, SENSOR_RADIUS).rect - view.overlayParams = UdfpsOverlayParams(sensorBounds, 1920, 1080, 1f, Surface.ROTATION_0) + view.overlayParams = UdfpsOverlayParams(sensorBounds, sensorBounds, 1920, + 1080, 1f, Surface.ROTATION_0) view.setUdfpsDisplayModeProvider(hbmProvider) ViewUtils.attachView(view) } @@ -133,7 +134,8 @@ class UdfpsViewTest : SysuiTestCase() { @Test fun isNotWithinSensorArea() { whenever(animationViewController.touchTranslation).thenReturn(PointF(0f, 0f)) - assertThat(view.isWithinSensorArea(SENSOR_RADIUS * 2.5f, SENSOR_RADIUS.toFloat())).isFalse() + assertThat(view.isWithinSensorArea(SENSOR_RADIUS * 2.5f, SENSOR_RADIUS.toFloat())) + .isFalse() assertThat(view.isWithinSensorArea(SENSOR_RADIUS.toFloat(), SENSOR_RADIUS * 2.5f)).isFalse() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java index d96ca91e36bd..677c7bdcffe1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java @@ -40,6 +40,7 @@ import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.screenshot.TimeoutHandler; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -47,6 +48,7 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +@Ignore("b/254635291") @SmallTest @RunWith(AndroidJUnit4.class) public class ClipboardOverlayControllerTest extends SysuiTestCase { diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java index 6a55a60c2fda..5bbd8109d8f9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java @@ -16,6 +16,9 @@ package com.android.systemui.doze; +import static android.content.res.Configuration.UI_MODE_NIGHT_YES; +import static android.content.res.Configuration.UI_MODE_TYPE_CAR; + import static com.android.systemui.doze.DozeMachine.State.DOZE; import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD; import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_DOCKED; @@ -38,16 +41,17 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.UiModeManager; import android.content.res.Configuration; import android.hardware.display.AmbientDisplayConfiguration; import android.testing.AndroidTestingRunner; import android.testing.UiThreadTest; import android.view.Display; +import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; @@ -78,25 +82,30 @@ public class DozeMachineTest extends SysuiTestCase { @Mock private DozeHost mHost; @Mock - private UiModeManager mUiModeManager; + private DozeMachine.Part mPartMock; + @Mock + private DozeMachine.Part mAnotherPartMock; private DozeServiceFake mServiceFake; private WakeLockFake mWakeLockFake; - private AmbientDisplayConfiguration mConfigMock; - private DozeMachine.Part mPartMock; + private AmbientDisplayConfiguration mAmbientDisplayConfigMock; @Before public void setUp() { MockitoAnnotations.initMocks(this); mServiceFake = new DozeServiceFake(); mWakeLockFake = new WakeLockFake(); - mConfigMock = mock(AmbientDisplayConfiguration.class); - mPartMock = mock(DozeMachine.Part.class); + mAmbientDisplayConfigMock = mock(AmbientDisplayConfiguration.class); when(mDockManager.isDocked()).thenReturn(false); when(mDockManager.isHidden()).thenReturn(false); - mMachine = new DozeMachine(mServiceFake, mConfigMock, mWakeLockFake, - mWakefulnessLifecycle, mUiModeManager, mDozeLog, mDockManager, - mHost, new DozeMachine.Part[]{mPartMock}); + mMachine = new DozeMachine(mServiceFake, + mAmbientDisplayConfigMock, + mWakeLockFake, + mWakefulnessLifecycle, + mDozeLog, + mDockManager, + mHost, + new DozeMachine.Part[]{mPartMock, mAnotherPartMock}); } @Test @@ -108,7 +117,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_goesToDoze() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); mMachine.requestState(INITIALIZED); @@ -118,7 +127,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_goesToAod() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); mMachine.requestState(INITIALIZED); @@ -138,7 +147,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_afterDockPaused_goesToDoze() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); @@ -151,7 +160,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_alwaysOnSuppressed_alwaysOnDisabled_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); mMachine.requestState(INITIALIZED); @@ -162,7 +171,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_alwaysOnSuppressed_alwaysOnEnabled_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); mMachine.requestState(INITIALIZED); @@ -184,7 +193,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_alwaysOnSuppressed_alwaysOnDisabled_afterDockPaused_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); @@ -197,7 +206,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_alwaysOnSuppressed_alwaysOnEnabled_afterDockPaused_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); @@ -209,7 +218,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_goesToDoze() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); mMachine.requestState(INITIALIZED); mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION); mMachine.requestState(DOZE_PULSING); @@ -222,7 +231,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_goesToAoD() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); mMachine.requestState(INITIALIZED); mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION); mMachine.requestState(DOZE_PULSING); @@ -236,7 +245,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_alwaysOnSuppressed_goesToSuppressed() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); mMachine.requestState(INITIALIZED); mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION); mMachine.requestState(DOZE_PULSING); @@ -287,7 +296,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_afterDockPaused_goesToDoze() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); mMachine.requestState(INITIALIZED); @@ -303,7 +312,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_alwaysOnSuppressed_afterDockPaused_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); mMachine.requestState(INITIALIZED); @@ -471,7 +480,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testTransitionToInitialized_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); verify(mPartMock).transitionTo(UNINITIALIZED, INITIALIZED); @@ -481,7 +492,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testTransitionToFinish_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(FINISH); @@ -490,7 +503,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testDozeToDozeSuspendTriggers_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(DOZE); @@ -499,7 +514,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testDozeAoDToDozeSuspendTriggers_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(DOZE_AOD); @@ -508,7 +525,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testDozePulsingBrightDozeSuspendTriggers_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(DOZE_PULSING_BRIGHT); @@ -517,7 +536,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testDozeAodDockedDozeSuspendTriggers_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(DOZE_AOD_DOCKED); @@ -525,7 +546,35 @@ public class DozeMachineTest extends SysuiTestCase { } @Test + public void testOnConfigurationChanged_propagatesUiModeTypeToParts() { + Configuration newConfig = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(newConfig); + + verify(mPartMock).onUiModeTypeChanged(UI_MODE_TYPE_CAR); + verify(mAnotherPartMock).onUiModeTypeChanged(UI_MODE_TYPE_CAR); + } + + @Test + public void testOnConfigurationChanged_propagatesOnlyUiModeChangesToParts() { + Configuration newConfig = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(newConfig); + mMachine.onConfigurationChanged(newConfig); + + verify(mPartMock, times(1)).onUiModeTypeChanged(UI_MODE_TYPE_CAR); + verify(mAnotherPartMock, times(1)).onUiModeTypeChanged(UI_MODE_TYPE_CAR); + } + + @Test public void testDozeSuppressTriggers_screenState() { assertEquals(Display.STATE_OFF, DOZE_SUSPEND_TRIGGERS.screenState(null)); } + + @NonNull + private Configuration configWithCarNightUiMode() { + Configuration configuration = Configuration.EMPTY; + configuration.uiMode = UI_MODE_TYPE_CAR | UI_MODE_NIGHT_YES; + return configuration; + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java index 0f29dcd5a939..32b994538e12 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java @@ -10,14 +10,14 @@ * 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 andatest + * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.doze; -import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE; -import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE; +import static android.content.res.Configuration.UI_MODE_TYPE_CAR; +import static android.content.res.Configuration.UI_MODE_TYPE_NORMAL; import static com.android.systemui.doze.DozeMachine.State.DOZE; import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD; @@ -26,17 +26,16 @@ import static com.android.systemui.doze.DozeMachine.State.FINISH; import static com.android.systemui.doze.DozeMachine.State.INITIALIZED; import static com.android.systemui.doze.DozeMachine.State.UNINITIALIZED; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.UiModeManager; -import android.content.BroadcastReceiver; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; import android.hardware.display.AmbientDisplayConfiguration; import android.testing.AndroidTestingRunner; import android.testing.UiThreadTest; @@ -44,13 +43,13 @@ import android.testing.UiThreadTest; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.statusbar.phone.BiometricUnlockController; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.AdditionalMatchers; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -71,10 +70,6 @@ public class DozeSuppressorTest extends SysuiTestCase { @Mock private AmbientDisplayConfiguration mConfig; @Mock - private BroadcastDispatcher mBroadcastDispatcher; - @Mock - private UiModeManager mUiModeManager; - @Mock private Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy; @Mock private BiometricUnlockController mBiometricUnlockController; @@ -83,13 +78,6 @@ public class DozeSuppressorTest extends SysuiTestCase { private DozeMachine mDozeMachine; @Captor - private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor; - @Captor - private ArgumentCaptor<IntentFilter> mIntentFilterCaptor; - private BroadcastReceiver mBroadcastReceiver; - private IntentFilter mIntentFilter; - - @Captor private ArgumentCaptor<DozeHost.Callback> mDozeHostCaptor; private DozeHost.Callback mDozeHostCallback; @@ -106,8 +94,6 @@ public class DozeSuppressorTest extends SysuiTestCase { mDozeHost, mConfig, mDozeLog, - mBroadcastDispatcher, - mUiModeManager, mBiometricUnlockControllerLazy); mDozeSuppressor.setDozeMachine(mDozeMachine); @@ -122,36 +108,35 @@ public class DozeSuppressorTest extends SysuiTestCase { public void testRegistersListenersOnInitialized_unregisteredOnFinish() { // check that receivers and callbacks registered mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); - captureBroadcastReceiver(); captureDozeHostCallback(); // check that receivers and callbacks are unregistered mDozeSuppressor.transitionTo(INITIALIZED, FINISH); - verify(mBroadcastDispatcher).unregisterReceiver(mBroadcastReceiver); verify(mDozeHost).removeCallback(mDozeHostCallback); } @Test public void testSuspendTriggersDoze_carMode() { // GIVEN car mode - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); // WHEN dozing begins mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); // THEN doze continues with all doze triggers disabled. - verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS); + verify(mDozeMachine, atLeastOnce()).requestState(DOZE_SUSPEND_TRIGGERS); + verify(mDozeMachine, never()) + .requestState(AdditionalMatchers.not(eq(DOZE_SUSPEND_TRIGGERS))); } @Test public void testSuspendTriggersDoze_enterCarMode() { // GIVEN currently dozing mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); - captureBroadcastReceiver(); mDozeSuppressor.transitionTo(INITIALIZED, DOZE); // WHEN car mode entered - mBroadcastReceiver.onReceive(null, new Intent(ACTION_ENTER_CAR_MODE)); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); // THEN doze continues with all doze triggers disabled. verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS); @@ -160,13 +145,13 @@ public class DozeSuppressorTest extends SysuiTestCase { @Test public void testDozeResume_exitCarMode() { // GIVEN currently suspended, with AOD not enabled + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(false); mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); - captureBroadcastReceiver(); mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS); // WHEN exiting car mode - mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE)); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_NORMAL); // THEN doze is resumed verify(mDozeMachine).requestState(DOZE); @@ -175,19 +160,53 @@ public class DozeSuppressorTest extends SysuiTestCase { @Test public void testDozeAoDResume_exitCarMode() { // GIVEN currently suspended, with AOD not enabled + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(true); mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); - captureBroadcastReceiver(); mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS); // WHEN exiting car mode - mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE)); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_NORMAL); // THEN doze AOD is resumed verify(mDozeMachine).requestState(DOZE_AOD); } @Test + public void testUiModeDoesNotChange_noStateTransition() { + mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); + clearInvocations(mDozeMachine); + + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + + verify(mDozeMachine, times(1)).requestState(DOZE_SUSPEND_TRIGGERS); + verify(mDozeMachine, never()) + .requestState(AdditionalMatchers.not(eq(DOZE_SUSPEND_TRIGGERS))); + } + + @Test + public void testUiModeTypeChange_whenDozeMachineIsNotReady_doesNotDoAnything() { + when(mDozeMachine.isUninitializedOrFinished()).thenReturn(true); + + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + + verify(mDozeMachine, never()).requestState(any()); + } + + @Test + public void testUiModeTypeChange_CarModeEnabledAndDozeMachineNotReady_suspendsTriggersAfter() { + when(mDozeMachine.isUninitializedOrFinished()).thenReturn(true); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + verify(mDozeMachine, never()).requestState(any()); + + mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); + + verify(mDozeMachine, times(1)).requestState(DOZE_SUSPEND_TRIGGERS); + } + + @Test public void testEndDoze_unprovisioned() { // GIVEN device unprovisioned when(mDozeHost.isProvisioned()).thenReturn(false); @@ -276,14 +295,4 @@ public class DozeSuppressorTest extends SysuiTestCase { verify(mDozeHost).addCallback(mDozeHostCaptor.capture()); mDozeHostCallback = mDozeHostCaptor.getValue(); } - - private void captureBroadcastReceiver() { - verify(mBroadcastDispatcher).registerReceiver(mBroadcastReceiverCaptor.capture(), - mIntentFilterCaptor.capture()); - mBroadcastReceiver = mBroadcastReceiverCaptor.getValue(); - mIntentFilter = mIntentFilterCaptor.getValue(); - assertEquals(2, mIntentFilter.countActions()); - org.hamcrest.MatcherAssert.assertThat(() -> mIntentFilter.actionsIterator(), - containsInAnyOrder(ACTION_ENTER_CAR_MODE, ACTION_EXIT_CAR_MODE)); - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt index fc672016a886..65b44a14d2ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt @@ -17,11 +17,13 @@ package com.android.systemui.dump import androidx.test.filters.SmallTest +import com.android.systemui.CoreStartable import com.android.systemui.Dumpable import com.android.systemui.SysuiTestCase -import com.android.systemui.log.LogBuffer +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager import com.android.systemui.util.mockito.any +import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.mockito.Mock @@ -30,6 +32,8 @@ import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations import java.io.PrintWriter +import java.io.StringWriter +import javax.inject.Provider @SmallTest class DumpHandlerTest : SysuiTestCase() { @@ -66,7 +70,9 @@ class DumpHandlerTest : SysuiTestCase() { mContext, dumpManager, logBufferEulogizer, - mutableMapOf(), + mutableMapOf( + EmptyCoreStartable::class.java to Provider { EmptyCoreStartable() } + ), exceptionHandlerManager ) } @@ -154,4 +160,20 @@ class DumpHandlerTest : SysuiTestCase() { verify(buffer1).dump(pw, 0) verify(buffer2).dump(pw, 0) } -}
\ No newline at end of file + + @Test + fun testConfigDump() { + // GIVEN a StringPrintWriter + val stringWriter = StringWriter() + val spw = PrintWriter(stringWriter) + + // When a config dump is requested + dumpHandler.dump(spw, arrayOf("config")) + + assertThat(stringWriter.toString()).contains(EmptyCoreStartable::class.java.simpleName) + } + + private class EmptyCoreStartable : CoreStartable { + override fun start() {} + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt index bd029a727ee3..64547f4463d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt @@ -16,9 +16,9 @@ package com.android.systemui.dump -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogcatEchoTracker /** * Creates a LogBuffer that will echo everything to logcat, which is useful for debugging tests. diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt new file mode 100644 index 000000000000..1b34100b1cef --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import android.animation.AnimationHandler.AnimationFrameCallbackProvider +import android.animation.ValueAnimator +import android.util.Log +import android.util.Log.TerribleFailure +import android.util.Log.TerribleFailureHandler +import android.view.Choreographer.FrameCallback +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Interpolators +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.google.common.truth.Truth.assertThat +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.After +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardTransitionRepositoryTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardTransitionRepository + private lateinit var oldWtfHandler: TerribleFailureHandler + private lateinit var wtfHandler: WtfHandler + + @Before + fun setUp() { + underTest = KeyguardTransitionRepository() + wtfHandler = WtfHandler() + oldWtfHandler = Log.setWtfHandler(wtfHandler) + } + + @After + fun tearDown() { + oldWtfHandler?.let { Log.setWtfHandler(it) } + } + + @Test + fun `startTransition runs animator to completion`() = + runBlocking(IMMEDIATE) { + val (animator, provider) = setupAnimator(this) + + val steps = mutableListOf<TransitionStep>() + val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this) + + underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator)) + + val startTime = System.currentTimeMillis() + while (animator.isRunning()) { + yield() + if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) { + fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION") + } + } + + assertSteps(steps, listWithStep(BigDecimal(.1))) + + job.cancel() + provider.stop() + } + + @Test + fun `startTransition called during another transition fails`() { + underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, null)) + underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, BOUNCER, null)) + + assertThat(wtfHandler.failed).isTrue() + } + + @Test + fun `Null animator enables manual control with updateTransition`() = + runBlocking(IMMEDIATE) { + val steps = mutableListOf<TransitionStep>() + val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this) + + val uuid = + underTest.startTransition( + TransitionInfo( + ownerName = OWNER_NAME, + from = AOD, + to = LOCKSCREEN, + animator = null, + ) + ) + + checkNotNull(uuid).let { + underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) + underTest.updateTransition(it, 1f, TransitionState.FINISHED) + } + + assertThat(steps.size).isEqualTo(3) + assertThat(steps[0]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED)) + assertThat(steps[1]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0.5f, TransitionState.RUNNING)) + assertThat(steps[2]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED)) + job.cancel() + } + + @Test + fun `Attempt to manually update transition with invalid UUID throws exception`() { + underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING) + assertThat(wtfHandler.failed).isTrue() + } + + @Test + fun `Attempt to manually update transition after FINISHED state throws exception`() { + val uuid = + underTest.startTransition( + TransitionInfo( + ownerName = OWNER_NAME, + from = AOD, + to = LOCKSCREEN, + animator = null, + ) + ) + + checkNotNull(uuid).let { + underTest.updateTransition(it, 1f, TransitionState.FINISHED) + underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) + } + assertThat(wtfHandler.failed).isTrue() + } + + private fun listWithStep(step: BigDecimal): List<BigDecimal> { + val steps = mutableListOf<BigDecimal>() + + var i = BigDecimal.ZERO + while (i.compareTo(BigDecimal.ONE) <= 0) { + steps.add(i) + i = (i + step).setScale(2, RoundingMode.HALF_UP) + } + + return steps + } + + private fun assertSteps(steps: List<TransitionStep>, fractions: List<BigDecimal>) { + // + 2 accounts for start and finish of automated transition + assertThat(steps.size).isEqualTo(fractions.size + 2) + + assertThat(steps[0]).isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED)) + fractions.forEachIndexed { index, fraction -> + assertThat(steps[index + 1]) + .isEqualTo( + TransitionStep(AOD, LOCKSCREEN, fraction.toFloat(), TransitionState.RUNNING) + ) + } + assertThat(steps[steps.size - 1]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED)) + + assertThat(wtfHandler.failed).isFalse() + } + + private fun setupAnimator( + scope: CoroutineScope + ): Pair<ValueAnimator, TestFrameCallbackProvider> { + val animator = + ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(ANIMATION_DURATION) + } + + val provider = TestFrameCallbackProvider(animator, scope) + provider.start() + + return Pair(animator, provider) + } + + /** Gives direct control over ValueAnimator. See [AnimationHandler] */ + private class TestFrameCallbackProvider( + private val animator: ValueAnimator, + private val scope: CoroutineScope, + ) : AnimationFrameCallbackProvider { + + private var frameCount = 1L + private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null)) + private var job: Job? = null + + fun start() { + animator.getAnimationHandler().setProvider(this) + + job = + scope.launch { + frames.collect { + // Delay is required for AnimationHandler to properly register a callback + delay(1) + val (frameNumber, callback) = it + callback?.doFrame(frameNumber) + } + } + } + + fun stop() { + job?.cancel() + animator.getAnimationHandler().setProvider(null) + } + + override fun postFrameCallback(cb: FrameCallback) { + frames.value = Pair(++frameCount, cb) + } + override fun postCommitCallback(runnable: Runnable) {} + override fun getFrameTime() = frameCount + override fun getFrameDelay() = 1L + override fun setFrameDelay(delay: Long) {} + } + + private class WtfHandler : TerribleFailureHandler { + var failed = false + override fun onTerribleFailure(tag: String, what: TerribleFailure, system: Boolean) { + failed = true + } + } + + companion object { + private const val MAX_TEST_DURATION = 100L + private const val ANIMATION_DURATION = 10L + private const val OWNER_NAME = "Test" + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt index 1078cdaa57c4..e009e8651f2a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt @@ -19,9 +19,9 @@ package com.android.systemui.media.taptotransfer.common import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogcatEchoTracker import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt index 7c83cb74bb77..6a4c0f60466d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt @@ -22,6 +22,9 @@ import android.graphics.drawable.Drawable import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -62,6 +65,34 @@ class MediaTttUtilsTest : SysuiTestCase() { } @Test + fun getIconFromPackageName_nullPackageName_returnsDefault() { + val icon = MediaTttUtils.getIconFromPackageName(context, appPackageName = null, logger) + + val expectedDesc = + ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name) + .loadContentDescription(context) + assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc) + } + + @Test + fun getIconFromPackageName_invalidPackageName_returnsDefault() { + val icon = MediaTttUtils.getIconFromPackageName(context, "fakePackageName", logger) + + val expectedDesc = + ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name) + .loadContentDescription(context) + assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc) + } + + @Test + fun getIconFromPackageName_validPackageName_returnsAppInfo() { + val icon = MediaTttUtils.getIconFromPackageName(context, PACKAGE_NAME, logger) + + assertThat(icon) + .isEqualTo(Icon.Loaded(appIconFromPackageName, ContentDescription.Loaded(APP_NAME))) + } + + @Test fun getIconInfoFromPackageName_nullPackageName_returnsDefault() { val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, appPackageName = null, logger) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt index 110bbb80df3a..f977f55db483 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt @@ -17,6 +17,9 @@ package com.android.systemui.media.taptotransfer.sender import android.app.StatusBarManager +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable import android.media.MediaRoute2Info import android.os.PowerManager import android.testing.AndroidTestingRunner @@ -25,6 +28,7 @@ import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager +import android.widget.ImageView import android.widget.TextView import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake @@ -32,16 +36,17 @@ import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.Text.Companion.loadText import com.android.systemui.media.taptotransfer.MediaTttFlags import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.view.ViewUtil import com.google.common.truth.Truth.assertThat @@ -60,20 +65,29 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class MediaTttSenderCoordinatorTest : SysuiTestCase() { + + // Note: This tests are a bit like integration tests because they use a real instance of + // [ChipbarCoordinator] and verify that the coordinator displays the correct view, based on + // the inputs from [MediaTttSenderCoordinator]. + private lateinit var underTest: MediaTttSenderCoordinator @Mock private lateinit var accessibilityManager: AccessibilityManager + @Mock private lateinit var applicationInfo: ApplicationInfo @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var falsingManager: FalsingManager @Mock private lateinit var falsingCollector: FalsingCollector @Mock private lateinit var logger: MediaTttLogger @Mock private lateinit var mediaTttFlags: MediaTttFlags + @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var powerManager: PowerManager @Mock private lateinit var viewUtil: ViewUtil @Mock private lateinit var windowManager: WindowManager private lateinit var chipbarCoordinator: ChipbarCoordinator private lateinit var commandQueueCallback: CommandQueue.Callbacks + private lateinit var fakeAppIconDrawable: Drawable private lateinit var fakeClock: FakeSystemClock private lateinit var fakeExecutor: FakeExecutor private lateinit var uiEventLoggerFake: UiEventLoggerFake @@ -85,6 +99,18 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true) whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) + fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!! + whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME) + whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable) + whenever( + packageManager.getApplicationInfo( + eq(PACKAGE_NAME), + any<PackageManager.ApplicationInfoFlags>() + ) + ) + .thenReturn(applicationInfo) + context.setMockPackageManager(packageManager) + fakeClock = FakeSystemClock() fakeExecutor = FakeExecutor(fakeClock) @@ -100,7 +126,6 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { accessibilityManager, configurationController, powerManager, - uiEventLogger, falsingManager, falsingCollector, viewUtil, @@ -149,8 +174,15 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(almostCloseToStartCast().state.getChipTextString(context, OTHER_DEVICE_NAME)) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST.id) } @@ -163,8 +195,15 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(almostCloseToEndCast().state.getChipTextString(context, OTHER_DEVICE_NAME)) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST.id) } @@ -177,10 +216,15 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED.id) } @@ -193,10 +237,15 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED.id) } @@ -209,15 +258,69 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToReceiverSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id) } @Test + fun transferToReceiverSucceeded_nullUndoCallback_noUndo() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun transferToReceiverSucceeded_withUndoRunnable_undoVisible() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue() + } + + @Test + fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { + var undoCallbackCalled = false + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() { + undoCallbackCalled = true + } + }, + ) + + getChipbarView().getUndoButton().performClick() + + // Event index 1 since initially displaying the succeeded chip would also log an event + assertThat(uiEventLoggerFake.eventId(1)) + .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id) + assertThat(undoCallbackCalled).isTrue() + assertThat(getChipbarView().getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) + } + + @Test fun commandQueueCallback_transferToThisDeviceSucceeded_triggersCorrectChip() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, @@ -225,15 +328,71 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToThisDeviceSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id) } @Test + fun transferToThisDeviceSucceeded_nullUndoCallback_noUndo() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun transferToThisDeviceSucceeded_withUndoRunnable_undoVisible() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue() + } + + @Test + fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { + var undoCallbackCalled = false + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() { + undoCallbackCalled = true + } + }, + ) + + getChipbarView().getUndoButton().performClick() + + // Event index 1 since initially displaying the succeeded chip would also log an event + assertThat(uiEventLoggerFake.eventId(1)) + .isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id + ) + assertThat(undoCallbackCalled).isTrue() + assertThat(getChipbarView().getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + } + + @Test fun commandQueueCallback_transferToReceiverFailed_triggersCorrectChip() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED, @@ -241,10 +400,15 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToReceiverFailed().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id) } @@ -257,10 +421,15 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo( - transferToThisDeviceFailed().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id) } @@ -407,53 +576,113 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { verify(windowManager).removeView(any()) } - private fun getChipView(): ViewGroup { + @Test + fun transferToReceiverSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText()) + + // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should + // verify that the new state it triggers operates just like any other state. + getChipbarView().getUndoButton().performClick() + fakeExecutor.runAllReady() + + // Verify that the click updated us to the triggered state + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + fakeExecutor.runAllReady() + + // Verify that we didn't remove the chipbar because it's in the triggered state + verify(windowManager, never()).removeView(any()) + verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + // Verify we eventually remove the chipbar + verify(windowManager).removeView(any()) + } + + @Test + fun transferToThisDeviceSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText()) + + // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should + // verify that the new state it triggers operates just like any other state. + getChipbarView().getUndoButton().performClick() + fakeExecutor.runAllReady() + + // Verify that the click updated us to the triggered state + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + fakeExecutor.runAllReady() + + // Verify that we didn't remove the chipbar because it's in the triggered state + verify(windowManager, never()).removeView(any()) + verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + // Verify we eventually remove the chipbar + verify(windowManager).removeView(any()) + } + + private fun getChipbarView(): ViewGroup { val viewCaptor = ArgumentCaptor.forClass(View::class.java) verify(windowManager).addView(viewCaptor.capture(), any()) return viewCaptor.value as ViewGroup } + private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.start_icon) + private fun ViewGroup.getChipText(): String = (this.requireViewById<TextView>(R.id.text)).text as String - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToStartCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToEndCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo) + private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading) - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo) + private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error) - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo) + private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.end_button) - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) + private fun ChipStateSender.getExpectedStateText(): String? { + return this.getChipTextString(context, OTHER_DEVICE_NAME).loadText(context) + } } +private const val APP_NAME = "Fake app name" private const val OTHER_DEVICE_NAME = "My Tablet" +private const val PACKAGE_NAME = "com.android.systemui" private const val TIMEOUT = 10000 private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME) .addFeature("feature") - .setClientPackageName("com.android.systemui") + .setClientPackageName(PACKAGE_NAME) .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java index 0badd861787d..1bc4719c70b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java @@ -147,6 +147,18 @@ public class ColorSchemeTest extends SysuiTestCase { } @Test + public void testMonochromatic() { + int colorInt = 0xffB3588A; // H350 C50 T50 + ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */, + Style.MONOCHROMATIC /* style */); + int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2); + Assert.assertTrue( + Color.red(neutralMid) == Color.green(neutralMid) + && Color.green(neutralMid) == Color.blue(neutralMid) + ); + } + + @Test @SuppressWarnings("ResultOfMethodCallIgnored") public void testToString() { new ColorScheme(Color.TRANSPARENT, false /* darkTheme */).toString(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt index aacc695ef301..68c10f20f6f7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt @@ -20,7 +20,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogcatEchoTracker import com.android.systemui.statusbar.disableflags.DisableFlagsLogger import com.google.common.truth.Truth.assertThat import java.io.PrintWriter diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java index 1c686c66e31e..5e9c1aaad309 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java @@ -22,7 +22,6 @@ import static junit.framework.Assert.assertNotNull; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -52,6 +51,7 @@ import android.text.SpannableStringBuilder; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; import android.widget.TextView; import com.android.systemui.R; @@ -97,6 +97,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { private static final int DEFAULT_ICON_ID = R.drawable.ic_info_outline; private ViewGroup mRootView; + private ViewGroup mSecurityFooterView; private TextView mFooterText; private TestableImageView mPrimaryFooterIcon; private QSSecurityFooter mFooter; @@ -121,21 +122,26 @@ public class QSSecurityFooterTest extends SysuiTestCase { Looper looper = mTestableLooper.getLooper(); Handler mainHandler = new Handler(looper); when(mUserTracker.getUserInfo()).thenReturn(mock(UserInfo.class)); - mRootView = (ViewGroup) new LayoutInflaterBuilder(mContext) + mSecurityFooterView = (ViewGroup) new LayoutInflaterBuilder(mContext) .replace("ImageView", TestableImageView.class) .build().inflate(R.layout.quick_settings_security_footer, null, false); mFooterUtils = new QSSecurityFooterUtils(getContext(), getContext().getSystemService(DevicePolicyManager.class), mUserTracker, mainHandler, mActivityStarter, mSecurityController, looper, mDialogLaunchAnimator); - mFooter = new QSSecurityFooter(mRootView, mainHandler, mSecurityController, looper, - mBroadcastDispatcher, mFooterUtils); - mFooterText = mRootView.findViewById(R.id.footer_text); - mPrimaryFooterIcon = mRootView.findViewById(R.id.primary_footer_icon); + mFooter = new QSSecurityFooter(mSecurityFooterView, mainHandler, mSecurityController, + looper, mBroadcastDispatcher, mFooterUtils); + mFooterText = mSecurityFooterView.findViewById(R.id.footer_text); + mPrimaryFooterIcon = mSecurityFooterView.findViewById(R.id.primary_footer_icon); when(mSecurityController.getDeviceOwnerComponentOnAnyUser()) .thenReturn(DEVICE_OWNER_COMPONENT); when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT)) .thenReturn(DEVICE_OWNER_TYPE_DEFAULT); + + // mSecurityFooterView must have a ViewGroup parent so that + // DialogLaunchAnimator.Controller.fromView() does not return null. + mRootView = new FrameLayout(mContext); + mRootView.addView(mSecurityFooterView); ViewUtils.attachView(mRootView); mFooter.init(); @@ -153,7 +159,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertEquals(View.GONE, mRootView.getVisibility()); + assertEquals(View.GONE, mSecurityFooterView.getVisibility()); } @Test @@ -165,7 +171,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { TestableLooper.get(this).processAllMessages(); assertEquals(mContext.getString(R.string.quick_settings_disclosure_management), mFooterText.getText()); - assertEquals(View.VISIBLE, mRootView.getVisibility()); + assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility()); assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility()); assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource()); } @@ -181,7 +187,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { assertEquals(mContext.getString(R.string.quick_settings_disclosure_named_management, MANAGING_ORGANIZATION), mFooterText.getText()); - assertEquals(View.VISIBLE, mRootView.getVisibility()); + assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility()); assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility()); assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource()); } @@ -200,7 +206,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { assertEquals(mContext.getString( R.string.quick_settings_financed_disclosure_named_management, MANAGING_ORGANIZATION), mFooterText.getText()); - assertEquals(View.VISIBLE, mRootView.getVisibility()); + assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility()); assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility()); assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource()); } @@ -217,7 +223,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertEquals(View.GONE, mRootView.getVisibility()); + assertEquals(View.GONE, mSecurityFooterView.getVisibility()); } @Test @@ -227,8 +233,8 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertFalse(mRootView.isClickable()); - assertEquals(View.GONE, mRootView.findViewById(R.id.footer_icon).getVisibility()); + assertFalse(mSecurityFooterView.isClickable()); + assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility()); } @Test @@ -241,8 +247,9 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertTrue(mRootView.isClickable()); - assertEquals(View.VISIBLE, mRootView.findViewById(R.id.footer_icon).getVisibility()); + assertTrue(mSecurityFooterView.isClickable()); + assertEquals(View.VISIBLE, + mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility()); } @Test @@ -254,8 +261,8 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertFalse(mRootView.isClickable()); - assertEquals(View.GONE, mRootView.findViewById(R.id.footer_icon).getVisibility()); + assertFalse(mSecurityFooterView.isClickable()); + assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility()); } @Test @@ -734,11 +741,11 @@ public class QSSecurityFooterTest extends SysuiTestCase { @Test public void testDialogUsesDialogLauncher() { when(mSecurityController.isDeviceManaged()).thenReturn(true); - mFooter.onClick(mRootView); + mFooter.onClick(mSecurityFooterView); mTestableLooper.processAllMessages(); - verify(mDialogLaunchAnimator).showFromView(any(), eq(mRootView), any()); + verify(mDialogLaunchAnimator).show(any(), any()); } @Test @@ -775,7 +782,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { ArgumentCaptor<AlertDialog> dialogCaptor = ArgumentCaptor.forClass(AlertDialog.class); mTestableLooper.processAllMessages(); - verify(mDialogLaunchAnimator).showFromView(dialogCaptor.capture(), any(), any()); + verify(mDialogLaunchAnimator).show(dialogCaptor.capture(), any()); AlertDialog dialog = dialogCaptor.getValue(); dialog.create(); @@ -817,8 +824,8 @@ public class QSSecurityFooterTest extends SysuiTestCase { verify(mBroadcastDispatcher).registerReceiverWithHandler(captor.capture(), any(), any(), any()); - // Pretend view is not visible temporarily - mRootView.onVisibilityAggregated(false); + // Pretend view is not attached anymore. + mRootView.removeView(mSecurityFooterView); captor.getValue().onReceive(mContext, new Intent(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG)); mTestableLooper.processAllMessages(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt index 3c258077c29d..2c2ddbb9b8c5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt @@ -23,13 +23,13 @@ import android.os.UserHandle import android.provider.Settings import android.testing.AndroidTestingRunner import android.testing.TestableLooper -import android.view.View import androidx.test.filters.SmallTest import com.android.internal.logging.nano.MetricsProto import com.android.internal.logging.testing.FakeMetricsLogger import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.globalactions.GlobalActionsDialogLite @@ -70,13 +70,13 @@ class FooterActionsInteractorTest : SysuiTestCase() { val underTest = utils.footerActionsInteractor(qsSecurityFooterUtils = qsSecurityFooterUtils) val quickSettingsContext = mock<Context>() - underTest.showDeviceMonitoringDialog(quickSettingsContext) - verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null) - val view = mock<View>() - whenever(view.context).thenReturn(quickSettingsContext) - underTest.showDeviceMonitoringDialog(view) + underTest.showDeviceMonitoringDialog(quickSettingsContext, null) verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null) + + val expandable = mock<Expandable>() + underTest.showDeviceMonitoringDialog(quickSettingsContext, expandable) + verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, expandable) } @Test @@ -85,8 +85,8 @@ class FooterActionsInteractorTest : SysuiTestCase() { val underTest = utils.footerActionsInteractor(uiEventLogger = uiEventLogger) val globalActionsDialogLite = mock<GlobalActionsDialogLite>() - val view = mock<View>() - underTest.showPowerMenuDialog(globalActionsDialogLite, view) + val expandable = mock<Expandable>() + underTest.showPowerMenuDialog(globalActionsDialogLite, expandable) // Event is logged. val logs = uiEventLogger.logs @@ -99,7 +99,7 @@ class FooterActionsInteractorTest : SysuiTestCase() { .showOrHideDialog( /* keyguardShowing= */ false, /* isDeviceProvisioned= */ true, - view, + expandable, ) } @@ -167,11 +167,11 @@ class FooterActionsInteractorTest : SysuiTestCase() { userSwitchDialogController = userSwitchDialogController, ) - val view = mock<View>() - underTest.showUserSwitcher(view) + val expandable = mock<Expandable>() + underTest.showUserSwitcher(context, expandable) // Dialog is shown. - verify(userSwitchDialogController).showDialog(view) + verify(userSwitchDialogController).showDialog(context, expandable) } @Test @@ -184,12 +184,9 @@ class FooterActionsInteractorTest : SysuiTestCase() { activityStarter = activityStarter, ) - // The clicked view. The context is necessary because it's used to build the intent, that - // we check below. - val view = mock<View>() - whenever(view.context).thenReturn(context) - - underTest.showUserSwitcher(view) + // The clicked expandable. + val expandable = mock<Expandable>() + underTest.showUserSwitcher(context, expandable) // Dialog is shown. val intentCaptor = argumentCaptor<Intent>() diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt index da52a9b1a3c2..bc27bbc13f81 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt @@ -33,6 +33,7 @@ import com.android.systemui.qs.QSUserSwitcherEvent import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.user.data.source.UserRecord import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -139,6 +140,11 @@ class UserDetailViewAdapterTest : SysuiTestCase() { clickableTest(false, false, mUserDetailItemView, true) } + @Test + fun testManageUsersIsNotAvailable() { + assertNull(adapter.users.find { it.isManageUsers }) + } + private fun createUserRecord(current: Boolean, guest: Boolean) = UserRecord( UserInfo(0 /* id */, "name", 0 /* flags */), diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt index 9d908fdfb976..0a34810f4d3f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt @@ -20,12 +20,12 @@ import android.content.DialogInterface import android.content.Intent import android.provider.Settings import android.testing.AndroidTestingRunner -import android.view.View import android.widget.Button import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.PseudoGridView @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -63,7 +64,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { @Mock private lateinit var userDetailViewAdapter: UserDetailView.Adapter @Mock - private lateinit var launchView: View + private lateinit var launchExpandable: Expandable @Mock private lateinit var neutralButton: Button @Mock @@ -79,7 +80,6 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - `when`(launchView.context).thenReturn(mContext) `when`(dialog.context).thenReturn(mContext) controller = UserSwitchDialogController( @@ -94,32 +94,34 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { @Test fun showDialog_callsDialogShow() { - controller.showDialog(launchView) - verify(dialogLaunchAnimator).showFromView(eq(dialog), eq(launchView), any(), anyBoolean()) + val launchController = mock<DialogLaunchAnimator.Controller>() + `when`(launchExpandable.dialogLaunchController(any())).thenReturn(launchController) + controller.showDialog(context, launchExpandable) + verify(dialogLaunchAnimator).show(eq(dialog), eq(launchController), anyBoolean()) verify(uiEventLogger).log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN) } @Test fun dialog_showForAllUsers() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog).setShowForAllUsers(true) } @Test fun dialog_cancelOnTouchOutside() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog).setCanceledOnTouchOutside(true) } @Test fun adapterAndGridLinked() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(userDetailViewAdapter).linkToViewGroup(any<PseudoGridView>()) } @Test fun doneButtonLogsCorrectly() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog).setPositiveButton(anyInt(), capture(clickCaptor)) @@ -132,7 +134,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun clickSettingsButton_noFalsing_opensSettings() { `when`(falsingManager.isFalseTap(anyInt())).thenReturn(false) - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog) .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */) @@ -153,7 +155,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun clickSettingsButton_Falsing_notOpensSettings() { `when`(falsingManager.isFalseTap(anyInt())).thenReturn(true) - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog) .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt index 0151822f871c..14a3bc147808 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt @@ -659,6 +659,51 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() { verify(privacyIconsController, never()).onParentInvisible() } + @Test + fun clockPivotYInCenter() { + val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java) + verify(clock).addOnLayoutChangeListener(capture(captor)) + var height = 100 + val width = 50 + + clock.executeLayoutChange(0, 0, width, height, captor.value) + verify(clock).pivotY = height.toFloat() / 2 + + height = 150 + clock.executeLayoutChange(0, 0, width, height, captor.value) + verify(clock).pivotY = height.toFloat() / 2 + } + + private fun View.executeLayoutChange( + left: Int, + top: Int, + right: Int, + bottom: Int, + listener: View.OnLayoutChangeListener + ) { + val oldLeft = this.left + val oldTop = this.top + val oldRight = this.right + val oldBottom = this.bottom + whenever(this.left).thenReturn(left) + whenever(this.top).thenReturn(top) + whenever(this.right).thenReturn(right) + whenever(this.bottom).thenReturn(bottom) + whenever(this.height).thenReturn(bottom - top) + whenever(this.width).thenReturn(right - left) + listener.onLayoutChange( + this, + oldLeft, + oldTop, + oldRight, + oldBottom, + left, + top, + right, + bottom + ) + } + private fun createWindowInsets( topCutout: Rect? = Rect() ): WindowInsets { diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index c0dae03023c5..e7138ab11cbf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -33,11 +33,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -76,6 +78,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.internal.util.CollectionUtils; import com.android.internal.util.LatencyTracker; +import com.android.keyguard.FaceAuthApiRequestReason; import com.android.keyguard.KeyguardClockSwitch; import com.android.keyguard.KeyguardClockSwitchController; import com.android.keyguard.KeyguardStatusView; @@ -93,7 +96,6 @@ import com.android.systemui.biometrics.AuthController; import com.android.systemui.camera.CameraGestureHelper; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; -import com.android.systemui.controls.dagger.ControlsComponent; import com.android.systemui.doze.DozeLog; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; @@ -109,7 +111,7 @@ import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QS; -import com.android.systemui.qrcodescanner.controller.QRCodeScannerController; +import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSFragment; import com.android.systemui.screenrecord.RecordingController; import com.android.systemui.shade.transition.ShadeTransitionController; @@ -165,7 +167,6 @@ import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.unfold.SysUIUnfoldComponent; import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.util.time.SystemClock; -import com.android.systemui.wallet.controller.QuickAccessWalletController; import com.android.wm.shell.animation.FlingAnimationUtils; import org.junit.After; @@ -173,6 +174,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; @@ -257,11 +260,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private KeyguardIndicationController mKeyguardIndicationController; @Mock private FragmentService mFragmentService; @Mock private FragmentHostManager mFragmentHostManager; - @Mock private QuickAccessWalletController mQuickAccessWalletController; - @Mock private QRCodeScannerController mQrCodeScannerController; @Mock private NotificationRemoteInputManager mNotificationRemoteInputManager; @Mock private RecordingController mRecordingController; - @Mock private ControlsComponent mControlsComponent; @Mock private LockscreenGestureLogger mLockscreenGestureLogger; @Mock private DumpManager mDumpManager; @Mock private InteractionJankMonitor mInteractionJankMonitor; @@ -282,6 +282,10 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private ViewTreeObserver mViewTreeObserver; @Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel; @Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor; + @Mock private MotionEvent mDownMotionEvent; + @Captor + private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener> + mEmptySpaceClickListenerCaptor; private NotificationPanelViewController.TouchHandler mTouchHandler; private ConfigurationController mConfigurationController; @@ -425,6 +429,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { when(mView.getViewTreeObserver()).thenReturn(mViewTreeObserver); when(mView.getParent()).thenReturn(mViewParent); when(mQs.getHeader()).thenReturn(mQsHeader); + when(mDownMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN); mMainHandler = new Handler(Looper.getMainLooper()); NotificationPanelViewController.PanelEventsEmitter panelEventsEmitter = @@ -512,6 +517,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { .addCallback(mNotificationPanelViewController.mStatusBarStateListener); mNotificationPanelViewController .setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class)); + verify(mNotificationStackScrollLayoutController) + .setOnEmptySpaceClickListener(mEmptySpaceClickListenerCaptor.capture()); } @After @@ -750,6 +757,38 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { } @Test + public void testOnTouchEvent_expansionResumesAfterBriefTouch() { + // Start shade collapse with swipe up + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */, + 0 /* metaState */)); + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_MOVE, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + assertThat(mNotificationPanelViewController.getClosing()).isTrue(); + assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue(); + + // simulate touch that does not exceed touch slop + onTouchEvent(MotionEvent.obtain(2L /* downTime */, + 2L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + mNotificationPanelViewController.setTouchSlopExceeded(false); + + onTouchEvent(MotionEvent.obtain(2L /* downTime */, + 2L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + // fling should still be called after a touch that does not exceed touch slop + assertThat(mNotificationPanelViewController.getClosing()).isTrue(); + assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue(); + } + + @Test public void handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() { when(mCommandQueue.panelsEnabled()).thenReturn(false); @@ -1540,6 +1579,103 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { ); } + @Test + public void onEmptySpaceClicked_notDozingAndOnKeyguard_requestsFaceAuth() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(KEYGUARD); + mNotificationPanelViewController.setDozing(false, false); + + // This sets the dozing state that is read when onMiddleClicked is eventually invoked. + mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mUpdateMonitor).requestFaceAuth(true, + FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED); + } + + @Test + public void onEmptySpaceClicked_notDozingAndFaceDetectionIsNotRunning_startsUnlockAnimation() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(KEYGUARD); + mNotificationPanelViewController.setDozing(false, false); + when(mUpdateMonitor.isFaceDetectionRunning()).thenReturn(false); + + // This sets the dozing state that is read when onMiddleClicked is eventually invoked. + mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mNotificationStackScrollLayoutController).setUnlockHintRunning(true); + } + + @Test + public void onEmptySpaceClicked_notDozingAndFaceDetectionIsRunning_doesNotStartUnlockHint() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(KEYGUARD); + mNotificationPanelViewController.setDozing(false, false); + when(mUpdateMonitor.isFaceDetectionRunning()).thenReturn(true); + + // This sets the dozing state that is read when onMiddleClicked is eventually invoked. + mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mNotificationStackScrollLayoutController, never()).setUnlockHintRunning(true); + } + + @Test + public void onEmptySpaceClicked_whenDozingAndOnKeyguard_doesNotRequestFaceAuth() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(KEYGUARD); + mNotificationPanelViewController.setDozing(true, false); + + // This sets the dozing state that is read when onMiddleClicked is eventually invoked. + mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString()); + } + + @Test + public void onEmptySpaceClicked_whenStatusBarShadeLocked_doesNotRequestFaceAuth() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(SHADE_LOCKED); + + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString()); + + } + + /** + * When shade is flinging to close and this fling is not intercepted, + * {@link AmbientState#setIsClosing(boolean)} should be called before + * {@link NotificationStackScrollLayoutController#onExpansionStopped()} + * to ensure scrollY can be correctly set to be 0 + */ + @Test + public void onShadeFlingClosingEnd_mAmbientStateSetClose_thenOnExpansionStopped() { + // Given: Shade is expanded + mNotificationPanelViewController.notifyExpandingFinished(); + mNotificationPanelViewController.setIsClosing(false); + + // When: Shade flings to close not canceled + mNotificationPanelViewController.notifyExpandingStarted(); + mNotificationPanelViewController.setIsClosing(true); + mNotificationPanelViewController.onFlingEnd(false); + + // Then: AmbientState's mIsClosing should be set to false + // before mNotificationStackScrollLayoutController.onExpansionStopped() is called + // to ensure NotificationStackScrollLayout.resetScrollPosition() -> resetScrollPosition + // -> setOwnScrollY(0) can set scrollY to 0 when shade is closed + InOrder inOrder = inOrder(mAmbientState, mNotificationStackScrollLayoutController); + inOrder.verify(mAmbientState).setIsClosing(false); + inOrder.verify(mNotificationStackScrollLayoutController).onExpansionStopped(); + } + private static MotionEvent createMotionEvent(int x, int y, int action) { return MotionEvent.obtain( /* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt index eb34561d15a0..cc45cf88fa18 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt @@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.animation.TextAnimator +import com.android.systemui.util.mockito.any import org.junit.Before import org.junit.Rule import org.junit.Test @@ -55,7 +56,7 @@ class AnimatableClockViewTest : SysuiTestCase() { clockView.animateAppearOnLockscreen() clockView.measure(50, 50) - verify(mockTextAnimator).glyphFilter = null + verify(mockTextAnimator).glyphFilter = any() verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, false, 350L, null, 0L, null) verifyNoMoreInteractions(mockTextAnimator) } @@ -66,7 +67,7 @@ class AnimatableClockViewTest : SysuiTestCase() { clockView.measure(50, 50) clockView.animateAppearOnLockscreen() - verify(mockTextAnimator, times(2)).glyphFilter = null + verify(mockTextAnimator, times(2)).glyphFilter = any() verify(mockTextAnimator).setTextStyle(100, -1.0f, 200, false, 0L, null, 0L, null) verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, true, 350L, null, 0L, null) verifyNoMoreInteractions(mockTextAnimator) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt index 5b34a95d4fb0..b761647e24e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt @@ -17,58 +17,58 @@ import org.mockito.MockitoAnnotations @SmallTest class UncaughtExceptionPreHandlerTest : SysuiTestCase() { - private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager + private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager - @Mock private lateinit var mockHandler: UncaughtExceptionHandler + @Mock private lateinit var mockHandler: UncaughtExceptionHandler - @Mock private lateinit var mockHandler2: UncaughtExceptionHandler + @Mock private lateinit var mockHandler2: UncaughtExceptionHandler - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - Thread.setUncaughtExceptionPreHandler(null) - preHandlerManager = UncaughtExceptionPreHandlerManager() - } + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + Thread.setUncaughtExceptionPreHandler(null) + preHandlerManager = UncaughtExceptionPreHandlerManager() + } - @Test - fun registerHandler_registersOnceOnly() { - preHandlerManager.registerHandler(mockHandler) - preHandlerManager.registerHandler(mockHandler) - preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) - verify(mockHandler, only()).uncaughtException(any(), any()) - } + @Test + fun registerHandler_registersOnceOnly() { + preHandlerManager.registerHandler(mockHandler) + preHandlerManager.registerHandler(mockHandler) + preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) + verify(mockHandler, only()).uncaughtException(any(), any()) + } - @Test - fun registerHandler_setsUncaughtExceptionPreHandler() { - Thread.setUncaughtExceptionPreHandler(null) - preHandlerManager.registerHandler(mockHandler) - assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull() - } + @Test + fun registerHandler_setsUncaughtExceptionPreHandler() { + Thread.setUncaughtExceptionPreHandler(null) + preHandlerManager.registerHandler(mockHandler) + assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull() + } - @Test - fun registerHandler_preservesOriginalHandler() { - Thread.setUncaughtExceptionPreHandler(mockHandler) - preHandlerManager.registerHandler(mockHandler2) - preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) - verify(mockHandler, only()).uncaughtException(any(), any()) - } + @Test + fun registerHandler_preservesOriginalHandler() { + Thread.setUncaughtExceptionPreHandler(mockHandler) + preHandlerManager.registerHandler(mockHandler2) + preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) + verify(mockHandler, only()).uncaughtException(any(), any()) + } - @Test - @Ignore - fun registerHandler_toleratesHandlersThatThrow() { - `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException()) - preHandlerManager.registerHandler(mockHandler2) - preHandlerManager.registerHandler(mockHandler) - preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) - verify(mockHandler2, only()).uncaughtException(any(), any()) - verify(mockHandler, only()).uncaughtException(any(), any()) - } + @Test + @Ignore + fun registerHandler_toleratesHandlersThatThrow() { + `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException()) + preHandlerManager.registerHandler(mockHandler2) + preHandlerManager.registerHandler(mockHandler) + preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception()) + verify(mockHandler2, only()).uncaughtException(any(), any()) + verify(mockHandler, only()).uncaughtException(any(), any()) + } - @Test - fun registerHandler_doesNotSetUpTwice() { - UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2) - assertThrows(IllegalStateException::class.java) { - preHandlerManager.registerHandler(mockHandler) + @Test + fun registerHandler_doesNotSetUpTwice() { + UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2) + assertThrows(IllegalStateException::class.java) { + preHandlerManager.registerHandler(mockHandler) + } } - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt index 8cb530c355bd..5fc0ffe42f55 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt @@ -4,7 +4,7 @@ import android.testing.AndroidTestingRunner import android.util.DisplayMetrics import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.log.LogBuffer +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.phone.LSShadeTransitionLogger import com.android.systemui.statusbar.phone.LockscreenGestureLogger diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java index f8a0d2fc415c..9c65fac1af45 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java @@ -70,7 +70,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; import com.android.systemui.telephony.TelephonyListenerManager; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java index ed8a3e16cdd1..4bed4a19b3d9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java @@ -38,7 +38,7 @@ import android.testing.TestableLooper.RunWithLooper; import com.android.settingslib.mobile.TelephonyIcons; import com.android.settingslib.net.DataUsageController; import com.android.systemui.dump.DumpManager; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.CarrierConfigTracker; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java index a76676e01c15..d5f5105036d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java @@ -43,7 +43,7 @@ import com.android.settingslib.mobile.TelephonyIcons; import com.android.settingslib.net.DataUsageController; import com.android.systemui.R; import com.android.systemui.dump.DumpManager; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.CarrierConfigTracker; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java index 82e32b2fdc64..09f8a10f88c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java @@ -34,10 +34,12 @@ import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -135,6 +137,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { public void setUp() { MockitoAnnotations.initMocks(this); allowTestableLooperAsMainThread(); + when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true); mListBuilder = new ShadeListBuilder( mDumpManager, @@ -1995,22 +1998,89 @@ public class ShadeListBuilderTest extends SysuiTestCase { } @Test + public void testActiveOrdering_withLegacyStability() { + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false); + assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change + assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X + assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change + assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X + assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap + } + + @Test + public void testStableOrdering_withLegacyStability() { + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false); + mStabilityManager.setAllowEntryReordering(false); + assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change + assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG", false); // X + assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // no change + assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG", false); // Z and X + assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG", false); // Z and X + gap + } + + @Test public void testStableOrdering() { + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true); mStabilityManager.setAllowEntryReordering(false); - assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG"); // X - assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG"); // no change - assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG"); // Z and X - assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG"); // Z and X + gap - verify(mStabilityManager, times(4)).onEntryReorderSuppressed(); + // No input or output + assertOrder("", "", "", true); + // Remove everything + assertOrder("ABCDEFG", "", "", true); + // Literally no changes + assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); + + // No stable order + assertOrder("", "ABCDEFG", "ABCDEFG", true); + + // F moved after A, and... + assertOrder("ABCDEFG", "AFBCDEG", "ABCDEFG", false); // No other changes + assertOrder("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false); // Insert X before F + assertOrder("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false); // Insert X after F + assertOrder("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false); // Insert X where F was + + // B moved after F, and... + assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // No other changes + assertOrder("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false); // Insert X before B + assertOrder("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false); // Insert X after B + assertOrder("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false); // Insert X where B was + + // Swap F and B, and... + assertOrder("ABCDEFG", "AFCDEBG", "ABCDEFG", false); // No other changes + assertOrder("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false); // Insert X before F + assertOrder("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false); // Insert X after F + assertOrder("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false); // Insert X between CD (or: ABCXDEFG) + assertOrder("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false); // Insert X between DE (or: ABCDEFXG) + assertOrder("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false); // Insert X before B + assertOrder("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false); // Insert X after B + + // Remove a bunch of entries at once + assertOrder("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true); + + // Remove a bunch of entries and scramble + assertOrder("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false); + + // Add a bunch of entries at once + assertOrder("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true); + + // Add a bunch of entries and reverse originals + // NOTE: Some of these don't have obviously correct answers + assertOrder("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false); // appended + assertOrder("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false); // prepended + assertOrder("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false); // closer to back: append + assertOrder("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false); // closer to front: prepend + assertOrder("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false); // split new entries + + // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout + assertOrder("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false); } @Test public void testActiveOrdering() { - assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG"); // X - assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG"); // no change - assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG"); // Z and X - assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG"); // Z and X + gap - verify(mStabilityManager, never()).onEntryReorderSuppressed(); + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true); + assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X + assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change + assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X + assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap } @Test @@ -2062,6 +2132,52 @@ public class ShadeListBuilderTest extends SysuiTestCase { } @Test + public void stableOrderingDisregardedWithSectionChange() { + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true); + // GIVEN the first sectioner's packages can be changed from run-to-run + List<String> mutableSectionerPackages = new ArrayList<>(); + mutableSectionerPackages.add(PACKAGE_1); + mListBuilder.setSectioners(asList( + new PackageSectioner(mutableSectionerPackages, null), + new PackageSectioner(List.of(PACKAGE_1, PACKAGE_2, PACKAGE_3), null))); + mStabilityManager.setAllowEntryReordering(false); + + // WHEN the list is originally built with reordering disabled (and section changes allowed) + addNotif(0, PACKAGE_1).setRank(4); + addNotif(1, PACKAGE_1).setRank(5); + addNotif(2, PACKAGE_2).setRank(1); + addNotif(3, PACKAGE_2).setRank(2); + addNotif(4, PACKAGE_3).setRank(3); + dispatchBuild(); + + // VERIFY the order and that entry reordering has not been suppressed + verifyBuiltList( + notif(0), + notif(1), + notif(2), + notif(3), + notif(4) + ); + verify(mStabilityManager, never()).onEntryReorderSuppressed(); + + // WHEN the first section now claims PACKAGE_3 notifications + mutableSectionerPackages.add(PACKAGE_3); + dispatchBuild(); + + // VERIFY the re-sectioned notification is inserted at #1 of the first section, which + // is the correct position based on its rank, rather than #3 in the new section simply + // because it was #3 in its previous section. + verifyBuiltList( + notif(4), + notif(0), + notif(1), + notif(2), + notif(3) + ); + verify(mStabilityManager, never()).onEntryReorderSuppressed(); + } + + @Test public void testStableChildOrdering() { // WHEN the list is originally built with reordering disabled mStabilityManager.setAllowEntryReordering(false); @@ -2112,6 +2228,85 @@ public class ShadeListBuilderTest extends SysuiTestCase { ); } + @Test + public void groupRevertingToSummaryDoesNotRetainStablePositionWithLegacyIndexLogic() { + when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(false); + + // GIVEN a notification group is on screen + mStabilityManager.setAllowEntryReordering(false); + + // WHEN the list is originally built with reordering disabled (and section changes allowed) + addNotif(0, PACKAGE_1).setRank(2); + addNotif(1, PACKAGE_1).setRank(3); + addGroupSummary(2, PACKAGE_1, "group").setRank(4); + addGroupChild(3, PACKAGE_1, "group").setRank(5); + addGroupChild(4, PACKAGE_1, "group").setRank(6); + dispatchBuild(); + + verifyBuiltList( + notif(0), + notif(1), + group( + summary(2), + child(3), + child(4) + ) + ); + + // WHEN the notification summary rank increases and children removed + setNewRank(notif(2).entry, 1); + mEntrySet.remove(4); + mEntrySet.remove(3); + dispatchBuild(); + + // VERIFY the summary (incorrectly) moves to the top of the section where it is ranked, + // despite visual stability being active + verifyBuiltList( + notif(2), + notif(0), + notif(1) + ); + } + + @Test + public void groupRevertingToSummaryRetainsStablePosition() { + when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true); + + // GIVEN a notification group is on screen + mStabilityManager.setAllowEntryReordering(false); + + // WHEN the list is originally built with reordering disabled (and section changes allowed) + addNotif(0, PACKAGE_1).setRank(2); + addNotif(1, PACKAGE_1).setRank(3); + addGroupSummary(2, PACKAGE_1, "group").setRank(4); + addGroupChild(3, PACKAGE_1, "group").setRank(5); + addGroupChild(4, PACKAGE_1, "group").setRank(6); + dispatchBuild(); + + verifyBuiltList( + notif(0), + notif(1), + group( + summary(2), + child(3), + child(4) + ) + ); + + // WHEN the notification summary rank increases and children removed + setNewRank(notif(2).entry, 1); + mEntrySet.remove(4); + mEntrySet.remove(3); + dispatchBuild(); + + // VERIFY the summary stays in the same location on rebuild + verifyBuiltList( + notif(0), + notif(1), + notif(2) + ); + } + private static void setNewRank(NotificationEntry entry, int rank) { entry.setRanking(new RankingBuilder(entry.getRanking()).setRank(rank).build()); } @@ -2255,26 +2450,35 @@ public class ShadeListBuilderTest extends SysuiTestCase { return addGroupChildWithTag(index, packageId, groupId, null); } - private void assertOrder(String visible, String active, String expected) { + private void assertOrder(String visible, String active, String expected, + boolean isOrderedCorrectly) { StringBuilder differenceSb = new StringBuilder(); + NotifSection section = new NotifSection(mock(NotifSectioner.class), 0); for (char c : active.toCharArray()) { if (visible.indexOf(c) < 0) differenceSb.append(c); } String difference = differenceSb.toString(); + int globalIndex = 0; for (int i = 0; i < visible.length(); i++) { - addNotif(i, String.valueOf(visible.charAt(i))) - .setRank(active.indexOf(visible.charAt(i))) + final char c = visible.charAt(i); + // Skip notifications which aren't active anymore + if (!active.contains(String.valueOf(c))) continue; + addNotif(globalIndex++, String.valueOf(c)) + .setRank(active.indexOf(c)) + .setSection(section) .setStableIndex(i); - } - for (int i = 0; i < difference.length(); i++) { - addNotif(i + visible.length(), String.valueOf(difference.charAt(i))) - .setRank(active.indexOf(difference.charAt(i))) + for (char c : difference.toCharArray()) { + addNotif(globalIndex++, String.valueOf(c)) + .setRank(active.indexOf(c)) + .setSection(section) .setStableIndex(-1); } + clearInvocations(mStabilityManager); + dispatchBuild(); StringBuilder resultSb = new StringBuilder(); for (int i = 0; i < expected.length(); i++) { @@ -2284,6 +2488,9 @@ public class ShadeListBuilderTest extends SysuiTestCase { assertEquals("visible [" + visible + "] active [" + active + "]", expected, resultSb.toString()); mEntrySet.clear(); + + verify(mStabilityManager, isOrderedCorrectly ? never() : times(1)) + .onEntryReorderSuppressed(); } private int nextId(String packageName) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 340bc96f80c2..3ff7639e9262 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -674,7 +674,9 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test fun testOnRankingApplied_newEntryShouldAlert() { // GIVEN that mEntry has never interrupted in the past, and now should + // and is new enough to do so assertFalse(mEntry.hasInterrupted()) + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis()) setShouldHeadsUp(mEntry) whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) @@ -690,8 +692,9 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test fun testOnRankingApplied_alreadyAlertedEntryShouldNotAlertAgain() { - // GIVEN that mEntry has alerted in the past + // GIVEN that mEntry has alerted in the past, even if it's new mEntry.setInterruption() + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis()) setShouldHeadsUp(mEntry) whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) @@ -725,6 +728,27 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(mHeadsUpManager).showNotification(mEntry) } + @Test + fun testOnRankingApplied_entryUpdatedButTooOld() { + // GIVEN that mEntry is added in a state where it should not HUN + setShouldHeadsUp(mEntry, false) + mCollectionListener.onEntryAdded(mEntry) + + // and it was actually added 10s ago + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis() - 10000) + + // WHEN it is updated to HUN and then a ranking update occurs + setShouldHeadsUp(mEntry) + whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) + mCollectionListener.onRankingApplied() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification is never bound or shown + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + verify(mHeadsUpManager, never()).showNotification(any()) + } + private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) { whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should) whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any())) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java index dcf245525f10..f4adf6927e31 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java @@ -181,7 +181,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testInflatesNewNotification() { // WHEN there is a new notification - mCollectionListener.onEntryInit(mEntry); + mCollectionListener.onEntryAdded(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); // THEN we inflate it @@ -194,7 +194,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testRebindsInflatedNotificationsOnUpdate() { // GIVEN an inflated notification - mCollectionListener.onEntryInit(mEntry); + mCollectionListener.onEntryAdded(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), any(), any()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); @@ -213,7 +213,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testEntrySmartReplyAdditionWillRebindViews() { // GIVEN an inflated notification - mCollectionListener.onEntryInit(mEntry); + mCollectionListener.onEntryAdded(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), any(), any()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); @@ -232,7 +232,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testEntryChangedToMinimizedSectionWillRebindViews() { // GIVEN an inflated notification - mCollectionListener.onEntryInit(mEntry); + mCollectionListener.onEntryAdded(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any()); assertFalse(mParamsCaptor.getValue().isLowPriority()); @@ -254,36 +254,28 @@ public class PreparationCoordinatorTest extends SysuiTestCase { public void testMinimizedEntryMovedIntoGroupWillRebindViews() { // GIVEN an inflated, minimized notification setSectionIsLowPriority(true); - mCollectionListener.onEntryInit(mEntry); + mCollectionListener.onEntryAdded(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any()); assertTrue(mParamsCaptor.getValue().isLowPriority()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); // WHEN notification is moved under a parent - NotificationEntry groupSummary = getNotificationEntryBuilder() - .setParent(ROOT_ENTRY) - .setGroupSummary(mContext, true) - .setGroup(mContext, TEST_GROUP_KEY) - .build(); - GroupEntry parent = mock(GroupEntry.class); - when(parent.getSummary()).thenReturn(groupSummary); - NotificationEntryBuilder.setNewParent(mEntry, parent); - mCollectionListener.onEntryInit(groupSummary); - mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry, groupSummary)); + NotificationEntryBuilder.setNewParent(mEntry, mock(GroupEntry.class)); + mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); // THEN we rebind it as not-minimized verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any()); assertFalse(mParamsCaptor.getValue().isLowPriority()); - // THEN we filter it because the parent summary is not yet inflated. - assertTrue(mUninflatedFilter.shouldFilterOut(mEntry, 0)); + // THEN we do not filter it because it's not the first inflation. + assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0)); } @Test public void testEntryRankChangeWillNotRebindViews() { // GIVEN an inflated notification - mCollectionListener.onEntryInit(mEntry); + mCollectionListener.onEntryAdded(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), any(), any()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); @@ -302,7 +294,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testDoesntFilterInflatedNotifs() { // GIVEN an inflated notification - mCollectionListener.onEntryInit(mEntry); + mCollectionListener.onEntryAdded(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), any(), any()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); @@ -338,9 +330,9 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mCollectionListener.onEntryInit(entry); } - mCollectionListener.onEntryInit(summary); + mCollectionListener.onEntryAdded(summary); for (NotificationEntry entry : children) { - mCollectionListener.onEntryInit(entry); + mCollectionListener.onEntryAdded(entry); } mBeforeFilterListener.onBeforeFinalizeFilter(List.of(groupEntry)); @@ -401,40 +393,6 @@ public class PreparationCoordinatorTest extends SysuiTestCase { } @Test - public void testPartiallyInflatedGroupsAreNotFilteredOutIfSummaryReinflate() { - // GIVEN a newly-posted group with a summary and two children - final String groupKey = "test_reinflate_group"; - final int summaryId = 1; - final GroupEntry group = new GroupEntryBuilder() - .setKey(groupKey) - .setCreationTime(400) - .setSummary(getNotificationEntryBuilder().setId(summaryId).setImportance(1).build()) - .addChild(getNotificationEntryBuilder().setId(2).build()) - .addChild(getNotificationEntryBuilder().setId(3).build()) - .build(); - fireAddEvents(List.of(group)); - final NotificationEntry summary = group.getSummary(); - final NotificationEntry child0 = group.getChildren().get(0); - final NotificationEntry child1 = group.getChildren().get(1); - mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group)); - - // WHEN all of the children (but not the summary) finish inflating - mNotifInflater.invokeInflateCallbackForEntry(child0); - mNotifInflater.invokeInflateCallbackForEntry(child1); - mNotifInflater.invokeInflateCallbackForEntry(summary); - - // WHEN the summary is updated and starts re-inflating - summary.setRanking(new RankingBuilder(summary.getRanking()).setImportance(4).build()); - fireUpdateEvents(summary); - mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group)); - - // THEN the entire group is still not filtered out - assertFalse(mUninflatedFilter.shouldFilterOut(summary, 401)); - assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401)); - assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401)); - } - - @Test public void testCompletedInflatedGroupsAreReleased() { // GIVEN a newly-posted group with a summary and two children final GroupEntry group = new GroupEntryBuilder() @@ -454,7 +412,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mNotifInflater.invokeInflateCallbackForEntry(child1); mNotifInflater.invokeInflateCallbackForEntry(summary); - // THEN the entire group is no longer filtered out + // THEN the entire group is still filtered out assertFalse(mUninflatedFilter.shouldFilterOut(summary, 401)); assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401)); assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401)); @@ -536,11 +494,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { private void fireAddEvents(NotificationEntry entry) { mCollectionListener.onEntryInit(entry); - mCollectionListener.onEntryInit(entry); - } - - private void fireUpdateEvents(NotificationEntry entry) { - mCollectionListener.onEntryUpdated(entry); + mCollectionListener.onEntryAdded(entry); } private static final String TEST_MESSAGE = "TEST_MESSAGE"; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt new file mode 100644 index 000000000000..1cdd023dd01c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.collection.listbuilder + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.util.Log +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class SemiStableSortTest : SysuiTestCase() { + + var shuffleInput: Boolean = false + var testStabilizeTo: Boolean = false + var sorter: SemiStableSort? = null + + @Before + fun setUp() { + shuffleInput = false + sorter = null + } + + private fun stringStabilizeTo( + stableOrder: String, + activeOrder: String, + ): Pair<String, Boolean> { + val actives = activeOrder.toMutableList() + val result = mutableListOf<Char>() + return (sorter ?: SemiStableSort()) + .stabilizeTo( + actives, + { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } }, + result, + ) + .let { ordered -> result.joinToString("") to ordered } + } + + private fun stringSort( + stableOrder: String, + activeOrder: String, + ): Pair<String, Boolean> { + val actives = activeOrder.toMutableList() + if (shuffleInput) { + actives.shuffle() + } + return (sorter ?: SemiStableSort()) + .sort( + actives, + { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } }, + compareBy { activeOrder.indexOf(it) }, + ) + .let { ordered -> actives.joinToString("") to ordered } + } + + private fun testCase( + stableOrder: String, + activeOrder: String, + expected: String, + expectOrdered: Boolean, + ) { + val (mergeResult, ordered) = + if (testStabilizeTo) stringStabilizeTo(stableOrder, activeOrder) + else stringSort(stableOrder, activeOrder) + val resultPass = expected == mergeResult + val orderedPass = ordered == expectOrdered + val pass = resultPass && orderedPass + val resultSuffix = + if (resultPass) "result=$expected" else "expected=$expected got=$mergeResult" + val orderedSuffix = + if (orderedPass) "ordered=$ordered" else "expected ordered to be $expectOrdered" + val readableResult = "stable=$stableOrder active=$activeOrder $resultSuffix $orderedSuffix" + Log.d("SemiStableSortTest", "${if (pass) "PASS" else "FAIL"}: $readableResult") + if (!pass) { + throw AssertionError("Test case failed: $readableResult") + } + } + + private fun runAllTestCases() { + // No input or output + testCase("", "", "", true) + // Remove everything + testCase("ABCDEFG", "", "", true) + // Literally no changes + testCase("ABCDEFG", "ABCDEFG", "ABCDEFG", true) + + // No stable order + testCase("", "ABCDEFG", "ABCDEFG", true) + + // F moved after A, and... + testCase("ABCDEFG", "AFBCDEG", "ABCDEFG", false) // No other changes + testCase("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false) // Insert X before F + testCase("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false) // Insert X after F + testCase("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false) // Insert X where F was + + // B moved after F, and... + testCase("ABCDEFG", "ACDEFBG", "ABCDEFG", false) // No other changes + testCase("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false) // Insert X before B + testCase("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false) // Insert X after B + testCase("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false) // Insert X where B was + + // Swap F and B, and... + testCase("ABCDEFG", "AFCDEBG", "ABCDEFG", false) // No other changes + testCase("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false) // Insert X before F + testCase("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false) // Insert X after F + testCase("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false) // Insert X between CD (Alt: ABCXDEFG) + testCase("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false) // Insert X between DE (Alt: ABCDEFXG) + testCase("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false) // Insert X before B + testCase("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false) // Insert X after B + + // Remove a bunch of entries at once + testCase("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true) + + // Remove a bunch of entries and scramble + testCase("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false) + + // Add a bunch of entries at once + testCase("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true) + + // Add a bunch of entries and reverse originals + // NOTE: Some of these don't have obviously correct answers + testCase("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false) // appended + testCase("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false) // prepended + testCase("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false) // closer to back: append + testCase("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false) // closer to front: prepend + testCase("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false) // split new entries + + // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout + testCase("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false) + } + + @Test + fun testSort() { + testStabilizeTo = false + shuffleInput = false + sorter = null + runAllTestCases() + } + + @Test + fun testSortWithSingleInstance() { + testStabilizeTo = false + shuffleInput = false + sorter = SemiStableSort() + runAllTestCases() + } + + @Test + fun testSortWithShuffledInput() { + testStabilizeTo = false + shuffleInput = true + sorter = null + runAllTestCases() + } + + @Test + fun testStabilizeTo() { + testStabilizeTo = true + sorter = null + runAllTestCases() + } + + @Test + fun testStabilizeToWithSingleInstance() { + testStabilizeTo = true + sorter = SemiStableSort() + runAllTestCases() + } + + @Test + fun testIsSorted() { + val intCmp = Comparator<Int> { x, y -> Integer.compare(x, y) } + SemiStableSort.apply { + assertTrue(emptyList<Int>().isSorted(intCmp)) + assertTrue(listOf(1).isSorted(intCmp)) + assertTrue(listOf(1, 2).isSorted(intCmp)) + assertTrue(listOf(1, 2, 3).isSorted(intCmp)) + assertTrue(listOf(1, 2, 3, 4).isSorted(intCmp)) + assertTrue(listOf(1, 2, 3, 4, 5).isSorted(intCmp)) + assertTrue(listOf(1, 1, 1, 1, 1).isSorted(intCmp)) + assertTrue(listOf(1, 1, 2, 2, 3, 3).isSorted(intCmp)) + assertFalse(listOf(2, 1).isSorted(intCmp)) + assertFalse(listOf(2, 1, 2).isSorted(intCmp)) + assertFalse(listOf(1, 2, 1).isSorted(intCmp)) + assertFalse(listOf(1, 2, 3, 2, 5).isSorted(intCmp)) + assertFalse(listOf(5, 2, 3, 4, 5).isSorted(intCmp)) + assertFalse(listOf(1, 2, 3, 4, 1).isSorted(intCmp)) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt new file mode 100644 index 000000000000..20369546d68a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.collection.listbuilder + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper.getContiguousSubLists +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class ShadeListBuilderHelperTest : SysuiTestCase() { + + @Test + fun testGetContiguousSubLists() { + assertThat(getContiguousSubLists("AAAAAA".toList()) { it }) + .containsExactly( + listOf('A', 'A', 'A', 'A', 'A', 'A'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABBB".toList()) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('B', 'B', 'B'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABAA".toList()) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('B'), + listOf('A', 'A'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABAA".toList(), minLength = 2) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('A', 'A'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList()) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('B', 'B', 'B', 'B'), + listOf('C', 'C'), + listOf('D'), + listOf('E', 'E', 'E'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList(), minLength = 2) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('B', 'B', 'B', 'B'), + listOf('C', 'C'), + listOf('E', 'E', 'E'), + ) + .inOrder() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt index 11798a7a4f96..87f4c323b7cc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt @@ -361,6 +361,22 @@ class AmbientStateTest : SysuiTestCase() { assertThat(sut.isOnKeyguard).isFalse() } // endregion + + // region mIsClosing + @Test + fun isClosing_whenShadeClosing_shouldReturnTrue() { + sut.setIsClosing(true) + + assertThat(sut.isClosing).isTrue() + } + + @Test + fun isClosing_whenShadeFinishClosing_shouldReturnFalse() { + sut.setIsClosing(false) + + assertThat(sut.isClosing).isFalse() + } + // endregion } // region Arrange helper methods. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 43530365360b..35c8b61b6383 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -728,6 +728,57 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { verify(mNotificationStackSizeCalculator).computeHeight(any(), anyInt(), anyFloat()); } + @Test + public void testSetOwnScrollY_shadeNotClosing_scrollYChanges() { + // Given: shade is not closing, scrollY is 0 + mAmbientState.setScrollY(0); + assertEquals(0, mAmbientState.getScrollY()); + mAmbientState.setIsClosing(false); + + // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1 + mStackScroller.setOwnScrollY(1); + + // Then: scrollY should be set to 1 + assertEquals(1, mAmbientState.getScrollY()); + + // Reset scrollY back to 0 to avoid interfering with other tests + mStackScroller.setOwnScrollY(0); + assertEquals(0, mAmbientState.getScrollY()); + } + + @Test + public void testSetOwnScrollY_shadeClosing_scrollYDoesNotChange() { + // Given: shade is closing, scrollY is 0 + mAmbientState.setScrollY(0); + assertEquals(0, mAmbientState.getScrollY()); + mAmbientState.setIsClosing(true); + + // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1 + mStackScroller.setOwnScrollY(1); + + // Then: scrollY should not change, it should still be 0 + assertEquals(0, mAmbientState.getScrollY()); + + // Reset scrollY and mAmbientState.mIsClosing to avoid interfering with other tests + mAmbientState.setIsClosing(false); + mStackScroller.setOwnScrollY(0); + assertEquals(0, mAmbientState.getScrollY()); + } + + @Test + public void onShadeFlingClosingEnd_scrollYShouldBeSetToZero() { + // Given: mAmbientState.mIsClosing is set to be true + // mIsExpanded is set to be false + mAmbientState.setIsClosing(true); + mStackScroller.setIsExpanded(false); + + // When: onExpansionStopped is called + mStackScroller.onExpansionStopped(); + + // Then: mAmbientState.scrollY should be set to be 0 + assertEquals(mAmbientState.getScrollY(), 0); + } + private void setBarStateForTest(int state) { // Can't inject this through the listener or we end up on the actual implementation // rather than the mock because the spy just coppied the anonymous inner /shruggie. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index ad497a2ec1e1..d4cd4a608340 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -98,7 +98,8 @@ import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; @@ -271,7 +272,6 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private OngoingCallController mOngoingCallController; @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager; @Mock private LockscreenShadeTransitionController mLockscreenTransitionController; - @Mock private FeatureFlags mFeatureFlags; @Mock private NotificationVisibilityProvider mVisibilityProvider; @Mock private WallpaperManager mWallpaperManager; @Mock private IWallpaperManager mIWallpaperManager; @@ -296,9 +296,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { private ShadeController mShadeController; private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); - private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); - private FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock); - private InitController mInitController = new InitController(); + private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); + private final FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock); + private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); + private final InitController mInitController = new InitController(); private final DumpManager mDumpManager = new DumpManager(); @Before @@ -1017,6 +1018,60 @@ public class CentralSurfacesImplTest extends SysuiTestCase { } @Test + public void collapseShade_callsAnimateCollapsePanels_whenExpanded() { + // GIVEN the shade is expanded + mCentralSurfaces.setPanelExpanded(true); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + + // WHEN collapseShade is called + mCentralSurfaces.collapseShade(); + + // VERIFY that animateCollapsePanels is called + verify(mShadeController).animateCollapsePanels(); + } + + @Test + public void collapseShade_doesNotCallAnimateCollapsePanels_whenCollapsed() { + // GIVEN the shade is collapsed + mCentralSurfaces.setPanelExpanded(false); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + + // WHEN collapseShade is called + mCentralSurfaces.collapseShade(); + + // VERIFY that animateCollapsePanels is NOT called + verify(mShadeController, never()).animateCollapsePanels(); + } + + @Test + public void collapseShadeForBugReport_callsAnimateCollapsePanels_whenFlagDisabled() { + // GIVEN the shade is expanded & flag enabled + mCentralSurfaces.setPanelExpanded(true); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, false); + + // WHEN collapseShadeForBugreport is called + mCentralSurfaces.collapseShadeForBugreport(); + + // VERIFY that animateCollapsePanels is called + verify(mShadeController).animateCollapsePanels(); + } + + @Test + public void collapseShadeForBugReport_doesNotCallAnimateCollapsePanels_whenFlagEnabled() { + // GIVEN the shade is expanded & flag enabled + mCentralSurfaces.setPanelExpanded(true); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, true); + + // WHEN collapseShadeForBugreport is called + mCentralSurfaces.collapseShadeForBugreport(); + + // VERIFY that animateCollapsePanels is called + verify(mShadeController, never()).animateCollapsePanels(); + } + + @Test public void deviceStateChange_unfolded_shadeOpen_setsLeaveOpenOnKeyguardHide() { when(mKeyguardStateController.isShowing()).thenReturn(false); setFoldedStates(FOLD_STATE_FOLDED); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt index 65e2964ea332..3a0a94ddd511 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt @@ -20,7 +20,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogcatEchoTracker import com.android.systemui.statusbar.disableflags.DisableFlagsLogger import com.google.common.truth.Truth.assertThat import java.io.PrintWriter diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 3a006adb1933..36e76f47ca4e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -49,9 +49,9 @@ import com.android.systemui.R; import com.android.systemui.SysuiBaseFragmentTest; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.log.LogBuffer; -import com.android.systemui.log.LogcatEchoTracker; import com.android.systemui.plugins.DarkIconDispatcher; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogcatEchoTracker; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.ShadeExpansionStateManager; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt index bf432388ad28..eba3b04f3472 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt @@ -20,7 +20,6 @@ import android.content.Intent import android.os.UserHandle import android.testing.AndroidTestingRunner import android.testing.TestableLooper -import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags @@ -34,8 +33,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -91,7 +90,7 @@ class StatusBarUserSwitcherControllerOldImplTest : SysuiTestCase() { fun testStartActivity() { `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(false) statusBarUserSwitcherContainer.callOnClick() - verify(userSwitcherDialogController).showDialog(any(View::class.java)) + verify(userSwitcherDialogController).showDialog(any(), any()) `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(true) statusBarUserSwitcherContainer.callOnClick() verify(activityStarter).startActivity(any(Intent::class.java), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt index 0e75c74ef6f5..b32058fca109 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt @@ -22,7 +22,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogcatEchoTracker import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange import com.google.common.truth.Truth.assertThat diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt index a3ad028519bb..e56623f9fea2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt @@ -22,7 +22,7 @@ import androidx.test.filters.SmallTest import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK @@ -125,19 +125,12 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase } else { testCase.expected.contentDescription.invoke(context) } - assertThat(iconFlow.value?.contentDescription?.getAsString()) + assertThat(iconFlow.value?.contentDescription?.loadContentDescription(context)) .isEqualTo(expectedContentDescription) job.cancel() } - private fun ContentDescription.getAsString(): String? { - return when (this) { - is ContentDescription.Loaded -> this.description - is ContentDescription.Resource -> context.getString(this.res) - } - } - internal data class Expected( /** The resource that should be used for the icon. */ @DrawableRes val iconResource: Int, diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt index c9f2b4db81ef..13e9f608158e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt @@ -19,9 +19,9 @@ package com.android.systemui.temporarydisplay import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogcatEchoTracker import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt index 6225d0c722ae..fa78b3832158 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt @@ -16,10 +16,6 @@ package com.android.systemui.temporarydisplay.chipbar -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.graphics.drawable.Drawable -import android.media.MediaRoute2Info import android.os.PowerManager import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -31,19 +27,18 @@ import android.widget.ImageView import android.widget.TextView import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake -import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text import com.android.systemui.media.taptotransfer.common.MediaTttLogger -import com.android.systemui.media.taptotransfer.sender.ChipStateSender -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEvents import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.view.ViewUtil import com.google.common.truth.Truth.assertThat @@ -53,7 +48,6 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock -import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -64,437 +58,277 @@ import org.mockito.MockitoAnnotations class ChipbarCoordinatorTest : SysuiTestCase() { private lateinit var underTest: FakeChipbarCoordinator - @Mock - private lateinit var packageManager: PackageManager - @Mock - private lateinit var applicationInfo: ApplicationInfo - @Mock - private lateinit var logger: MediaTttLogger - @Mock - private lateinit var accessibilityManager: AccessibilityManager - @Mock - private lateinit var configurationController: ConfigurationController - @Mock - private lateinit var powerManager: PowerManager - @Mock - private lateinit var windowManager: WindowManager - @Mock - private lateinit var falsingManager: FalsingManager - @Mock - private lateinit var falsingCollector: FalsingCollector - @Mock - private lateinit var viewUtil: ViewUtil - private lateinit var fakeAppIconDrawable: Drawable + @Mock private lateinit var logger: MediaTttLogger + @Mock private lateinit var accessibilityManager: AccessibilityManager + @Mock private lateinit var configurationController: ConfigurationController + @Mock private lateinit var powerManager: PowerManager + @Mock private lateinit var windowManager: WindowManager + @Mock private lateinit var falsingManager: FalsingManager + @Mock private lateinit var falsingCollector: FalsingCollector + @Mock private lateinit var viewUtil: ViewUtil private lateinit var fakeClock: FakeSystemClock private lateinit var fakeExecutor: FakeExecutor private lateinit var uiEventLoggerFake: UiEventLoggerFake - private lateinit var senderUiEventLogger: MediaTttSenderUiEventLogger @Before fun setUp() { MockitoAnnotations.initMocks(this) - - fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!! - whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME) - whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable) - whenever(packageManager.getApplicationInfo( - eq(PACKAGE_NAME), any<PackageManager.ApplicationInfoFlags>() - )).thenReturn(applicationInfo) - context.setMockPackageManager(packageManager) + whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) fakeClock = FakeSystemClock() fakeExecutor = FakeExecutor(fakeClock) uiEventLoggerFake = UiEventLoggerFake() - senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake) - - whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) - underTest = FakeChipbarCoordinator( - context, - logger, - windowManager, - fakeExecutor, - accessibilityManager, - configurationController, - powerManager, - senderUiEventLogger, - falsingManager, - falsingCollector, - viewUtil, - ) + underTest = + FakeChipbarCoordinator( + context, + logger, + windowManager, + fakeExecutor, + accessibilityManager, + configurationController, + powerManager, + falsingManager, + falsingCollector, + viewUtil, + ) underTest.start() } @Test - fun almostCloseToStartCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() { - val state = almostCloseToStartCast() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } + fun displayView_loadedIcon_correctlyRendered() { + val drawable = context.getDrawable(R.drawable.ic_celebration)!! - @Test - fun almostCloseToEndCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() { - val state = almostCloseToEndCast() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) + underTest.displayView( + ChipbarInfo( + Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")), + Text.Loaded("text"), + endItem = null, + ) ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - @Test - fun transferToReceiverTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() { - val state = transferToReceiverTriggered() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) + val iconView = getChipbarView().getStartIconView() + assertThat(iconView.drawable).isEqualTo(drawable) + assertThat(iconView.contentDescription).isEqualTo("loadedCD") } @Test - fun transferToThisDeviceTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() { - val state = transferToThisDeviceTriggered() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) + fun displayView_resourceIcon_correctlyRendered() { + val contentDescription = ContentDescription.Resource(R.string.controls_error_timeout) + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.drawable.ic_cake, contentDescription), + Text.Loaded("text"), + endItem = null, + ) ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - @Test - fun transferToReceiverSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() { - val state = transferToReceiverSucceeded() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) + val iconView = getChipbarView().getStartIconView() + assertThat(iconView.contentDescription) + .isEqualTo(contentDescription.loadContentDescription(context)) } @Test - fun transferToReceiverSucceeded_nullUndoRunnable_noUndo() { - underTest.displayView(transferToReceiverSucceeded(undoCallback = null)) + fun displayView_loadedText_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("display view text here"), + endItem = null, + ) + ) - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(getChipbarView().getChipText()).isEqualTo("display view text here") } @Test - fun transferToReceiverSucceeded_withUndoRunnable_undoWithClick() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue() - } + fun displayView_resourceText_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Resource(R.string.screenrecord_start_error), + endItem = null, + ) + ) - @Test - fun transferToReceiverSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() { - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() - - assertThat(undoCallbackCalled).isTrue() + assertThat(getChipbarView().getChipText()) + .isEqualTo(context.getString(R.string.screenrecord_start_error)) } @Test - fun transferToReceiverSucceeded_withUndoRunnable_falseTap_callbackNotRun() { - whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true) - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() + fun displayView_endItemNull_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = null, + ) + ) - assertThat(undoCallbackCalled).isFalse() + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) } @Test - fun transferToReceiverSucceeded_withUndoRunnable_realTap_callbackRun() { - whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false) - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() + fun displayView_endItemLoading_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Loading, + ) + ) - assertThat(undoCallbackCalled).isTrue() + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) } @Test - fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToReceiverSucceeded(undoCallback)) - - getChipView().getUndoButton().performClick() - - assertThat(getChipView().getChipText()).isEqualTo( - transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id + fun displayView_endItemError_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Error, + ) ) - } - @Test - fun transferToThisDeviceSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() { - val state = transferToThisDeviceSucceeded() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) } @Test - fun transferToThisDeviceSucceeded_nullUndoRunnable_noUndo() { - underTest.displayView(transferToThisDeviceSucceeded(undoCallback = null)) + fun displayView_endItemButton_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + onClickListener = {}, + ), + ) + ) - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().text).isEqualTo("button text") + assertThat(chipbarView.getEndButton().hasOnClickListeners()).isTrue() } @Test - fun transferToThisDeviceSucceeded_withUndoRunnable_undoWithClick() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToThisDeviceSucceeded(undoCallback)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue() - } - - @Test - fun transferToThisDeviceSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() { - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - underTest.displayView(transferToThisDeviceSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() - - assertThat(undoCallbackCalled).isTrue() - } + fun displayView_endItemButtonClicked_falseTap_listenerNotRun() { + whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true) + var isClicked = false + val buttonClickListener = View.OnClickListener { isClicked = true } - @Test - fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToReceiverTriggered() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - underTest.displayView(transferToThisDeviceSucceeded(undoCallback)) + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + buttonClickListener, + ), + ) + ) - getChipView().getUndoButton().performClick() + getChipbarView().getEndButton().performClick() - assertThat(getChipView().getChipText()).isEqualTo( - transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id - ) + assertThat(isClicked).isFalse() } @Test - fun transferToReceiverFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() { - val state = transferToReceiverFailed() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(getChipView().getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE) - } + fun displayView_endItemButtonClicked_notFalseTap_listenerRun() { + whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false) + var isClicked = false + val buttonClickListener = View.OnClickListener { isClicked = true } - @Test - fun transferToThisDeviceFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() { - val state = transferToThisDeviceFailed() - underTest.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(getChipView().getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + buttonClickListener, + ), + ) ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE) - } - @Test - fun changeFromAlmostCloseToStartToTransferTriggered_loadingIconAppears() { - underTest.displayView(almostCloseToStartCast()) - underTest.displayView(transferToReceiverTriggered()) + getChipbarView().getEndButton().performClick() - assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.VISIBLE) + assertThat(isClicked).isTrue() } @Test - fun changeFromTransferTriggeredToTransferSucceeded_loadingIconDisappears() { - underTest.displayView(transferToReceiverTriggered()) - underTest.displayView(transferToReceiverSucceeded()) - - assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.GONE) - } + fun updateView_viewUpdated() { + // First, display a view + val drawable = context.getDrawable(R.drawable.ic_celebration)!! - @Test - fun changeFromTransferTriggeredToTransferSucceeded_undoButtonAppears() { - underTest.displayView(transferToReceiverTriggered()) underTest.displayView( - transferToReceiverSucceeded( - object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } + ChipbarInfo( + Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")), + Text.Loaded("title text"), + endItem = ChipbarEndItem.Loading, ) ) - assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.VISIBLE) - } - - @Test - fun changeFromTransferSucceededToAlmostCloseToStart_undoButtonDisappears() { - underTest.displayView(transferToReceiverSucceeded()) - underTest.displayView(almostCloseToStartCast()) - - assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.GONE) - } - - @Test - fun changeFromTransferTriggeredToTransferFailed_failureIconAppears() { - underTest.displayView(transferToReceiverTriggered()) - underTest.displayView(transferToReceiverFailed()) + val chipbarView = getChipbarView() + assertThat(chipbarView.getStartIconView().drawable).isEqualTo(drawable) + assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("loadedCD") + assertThat(chipbarView.getChipText()).isEqualTo("title text") + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) + + // WHEN the view is updated + val newDrawable = context.getDrawable(R.drawable.ic_cake)!! + underTest.updateView( + ChipbarInfo( + Icon.Loaded(newDrawable, ContentDescription.Loaded("new CD")), + Text.Loaded("new title text"), + endItem = ChipbarEndItem.Error, + ), + chipbarView + ) - assertThat(getChipView().getFailureIcon().visibility).isEqualTo(View.VISIBLE) + // THEN we display the new view + assertThat(chipbarView.getStartIconView().drawable).isEqualTo(newDrawable) + assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("new CD") + assertThat(chipbarView.getChipText()).isEqualTo("new title text") + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) } - private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon) + private fun ViewGroup.getStartIconView() = this.requireViewById<ImageView>(R.id.start_icon) private fun ViewGroup.getChipText(): String = (this.requireViewById<TextView>(R.id.text)).text as String - private fun ViewGroup.getLoadingIconVisibility(): Int = - this.requireViewById<View>(R.id.loading).visibility + private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading) - private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.undo) + private fun ViewGroup.getEndButton(): TextView = this.requireViewById(R.id.end_button) - private fun ViewGroup.getFailureIcon(): View = this.requireViewById(R.id.failure_icon) + private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error) - private fun getChipView(): ViewGroup { + private fun getChipbarView(): ViewGroup { val viewCaptor = ArgumentCaptor.forClass(View::class.java) verify(windowManager).addView(viewCaptor.capture(), any()) return viewCaptor.value as ViewGroup } - - // TODO(b/245610654): For now, the below methods are duplicated between this test and - // [MediaTttSenderCoordinatorTest]. Once we define a generic API for [ChipbarCoordinator], - // these will no longer be duplicated. - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToStartCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToEndCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) } -private const val APP_NAME = "Fake app name" -private const val OTHER_DEVICE_NAME = "My Tablet" -private const val PACKAGE_NAME = "com.android.systemui" private const val TIMEOUT = 10000 - -private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME) - .addFeature("feature") - .setClientPackageName(PACKAGE_NAME) - .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt index 10704ac8fc67..8f32e0fd1de5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt @@ -24,7 +24,6 @@ import android.view.accessibility.AccessibilityManager import com.android.systemui.classifier.FalsingCollector import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor @@ -39,7 +38,6 @@ class FakeChipbarCoordinator( accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, powerManager: PowerManager, - uiEventLogger: MediaTttSenderUiEventLogger, falsingManager: FalsingManager, falsingCollector: FalsingCollector, viewUtil: ViewUtil, @@ -52,7 +50,6 @@ class FakeChipbarCoordinator( accessibilityManager, configurationController, powerManager, - uiEventLogger, falsingManager, falsingCollector, viewUtil, diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt index d4b41c18e123..a363a037c499 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt @@ -97,6 +97,7 @@ class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() { createUserRecord(2), createActionRecord(UserActionModel.ADD_SUPERVISED_USER), createActionRecord(UserActionModel.ENTER_GUEST_MODE), + createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT), ) ) var models: List<UserModel>? = null @@ -176,15 +177,17 @@ class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() { createUserRecord(2), createActionRecord(UserActionModel.ADD_SUPERVISED_USER), createActionRecord(UserActionModel.ENTER_GUEST_MODE), + createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT), ) ) var models: List<UserActionModel>? = null val job = underTest.actions.onEach { models = it }.launchIn(this) - assertThat(models).hasSize(3) + assertThat(models).hasSize(4) assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER) assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER) assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE) + assertThat(models?.get(3)).isEqualTo(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) job.cancel() } @@ -200,6 +203,7 @@ class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() { isAddUser = action == UserActionModel.ADD_USER, isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER, isGuest = action == UserActionModel.ENTER_GUEST_MODE, + isManageUsers = action == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt index c3a9705bf6ba..6a17c8ddc63d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt @@ -64,13 +64,7 @@ open class UserInteractorUnrefactoredTest : UserInteractorTest() { @Test fun `actions - not actionable when locked and not locked`() = runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) + setActions() userRepository.setActionableWhenLocked(false) keyguardRepository.setKeyguardShowing(false) @@ -92,13 +86,7 @@ open class UserInteractorUnrefactoredTest : UserInteractorTest() { @Test fun `actions - actionable when locked and not locked`() = runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) + setActions() userRepository.setActionableWhenLocked(true) keyguardRepository.setKeyguardShowing(false) @@ -120,13 +108,7 @@ open class UserInteractorUnrefactoredTest : UserInteractorTest() { @Test fun `actions - actionable when locked and locked`() = runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) + setActions() userRepository.setActionableWhenLocked(true) keyguardRepository.setKeyguardShowing(true) @@ -182,6 +164,10 @@ open class UserInteractorUnrefactoredTest : UserInteractorTest() { verify(activityStarter).startActivity(any(), anyBoolean()) } + private fun setActions() { + userRepository.setActions(UserActionModel.values().toList()) + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index 0344e3f991e2..c12a868dbaed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -268,6 +268,26 @@ class UserSwitcherViewModelTest : SysuiTestCase() { } @Test + fun `menu actions`() = + runBlocking(IMMEDIATE) { + userRepository.setActions(UserActionModel.values().toList()) + var actions: List<UserActionViewModel>? = null + val job = underTest.menu.onEach { actions = it }.launchIn(this) + + assertThat(actions?.map { it.viewKey }) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(), + UserActionModel.ADD_USER.ordinal.toLong(), + UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(), + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(), + ) + ) + + job.cancel() + } + + @Test fun `isFinishRequested - finishes when user is switched`() = runBlocking(IMMEDIATE) { setUsers(count = 2) diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt deleted file mode 100644 index 5e09b81da4e8..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.util.collection - -import android.testing.AndroidTestingRunner -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertSame -import org.junit.Assert.assertThrows -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.MockitoAnnotations - -@SmallTest -@RunWith(AndroidTestingRunner::class) -class RingBufferTest : SysuiTestCase() { - - private val buffer = RingBuffer(5) { TestElement() } - - private val history = mutableListOf<TestElement>() - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - } - - @Test - fun testBarelyFillBuffer() { - fillBuffer(5) - - assertEquals(0, buffer[0].id) - assertEquals(1, buffer[1].id) - assertEquals(2, buffer[2].id) - assertEquals(3, buffer[3].id) - assertEquals(4, buffer[4].id) - } - - @Test - fun testPartiallyFillBuffer() { - fillBuffer(3) - - assertEquals(3, buffer.size) - - assertEquals(0, buffer[0].id) - assertEquals(1, buffer[1].id) - assertEquals(2, buffer[2].id) - - assertThrows(IndexOutOfBoundsException::class.java) { buffer[3] } - assertThrows(IndexOutOfBoundsException::class.java) { buffer[4] } - } - - @Test - fun testSpinBuffer() { - fillBuffer(277) - - assertEquals(272, buffer[0].id) - assertEquals(273, buffer[1].id) - assertEquals(274, buffer[2].id) - assertEquals(275, buffer[3].id) - assertEquals(276, buffer[4].id) - assertThrows(IndexOutOfBoundsException::class.java) { buffer[5] } - - assertEquals(5, buffer.size) - } - - @Test - fun testElementsAreRecycled() { - fillBuffer(23) - - assertSame(history[4], buffer[1]) - assertSame(history[9], buffer[1]) - assertSame(history[14], buffer[1]) - assertSame(history[19], buffer[1]) - } - - @Test - fun testIterator() { - fillBuffer(13) - - val iterator = buffer.iterator() - - for (i in 0 until 5) { - assertEquals(history[8 + i], iterator.next()) - } - assertFalse(iterator.hasNext()) - assertThrows(NoSuchElementException::class.java) { iterator.next() } - } - - @Test - fun testForEach() { - fillBuffer(13) - var i = 8 - - buffer.forEach { - assertEquals(history[i], it) - i++ - } - assertEquals(13, i) - } - - private fun fillBuffer(count: Int) { - for (i in 0 until count) { - val elem = buffer.advance() - elem.id = history.size - history.add(elem) - } - } -} - -private class TestElement(var id: Int = 0) { - override fun toString(): String { - return "{TestElement $id}" - } -}
\ No newline at end of file diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 725b1f41372c..0c126805fb78 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.data.repository import com.android.systemui.common.shared.model.Position +import com.android.systemui.keyguard.shared.model.StatusBarState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -44,6 +45,9 @@ class FakeKeyguardRepository : KeyguardRepository { private val _dozeAmount = MutableStateFlow(0f) override val dozeAmount: Flow<Float> = _dozeAmount + private val _statusBarState = MutableStateFlow(StatusBarState.SHADE) + override val statusBarState: Flow<StatusBarState> = _statusBarState + override fun isKeyguardShowing(): Boolean { return _isKeyguardShowing.value } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt index 527258579372..c33ce5d9484d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt @@ -16,7 +16,7 @@ package com.android.systemui.qs -import android.view.View +import com.android.systemui.animation.Expandable import com.android.systemui.qs.FgsManagerController.OnDialogDismissedListener import com.android.systemui.qs.FgsManagerController.OnNumberOfPackagesChangedListener import kotlinx.coroutines.flow.MutableStateFlow @@ -54,7 +54,7 @@ class FakeFgsManagerController( override fun init() {} - override fun showDialog(viewLaunchedFrom: View?) {} + override fun showDialog(expandable: Expandable?) {} override fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) { numRunningPackagesListeners.add(listener) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt index 2a9aeddc9aa8..325da4ead666 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt @@ -57,7 +57,6 @@ import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.GlobalSettings -import com.android.systemui.util.time.FakeSystemClock import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.test.TestCoroutineDispatcher @@ -68,7 +67,6 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher class FooterActionsTestUtils( private val context: Context, private val testableLooper: TestableLooper, - private val fakeClock: FakeSystemClock = FakeSystemClock(), ) { /** Enable or disable the user switcher in the settings. */ fun setUserSwitcherEnabled(settings: GlobalSettings, enabled: Boolean, userId: Int) { diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto index a94bfe281be1..12e722601e25 100644 --- a/proto/src/system_messages.proto +++ b/proto/src/system_messages.proto @@ -61,7 +61,7 @@ message SystemMessage { // Notify the user that they should select an input method // Package: android - NOTE_SELECT_INPUT_METHOD = 8; + NOTE_SELECT_INPUT_METHOD = 8 [deprecated = true]; // Notify the user about limited functionality before decryption // Package: android diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java index ec30369bd099..02053cc7cfd3 100644 --- a/services/companion/java/com/android/server/companion/virtual/InputController.java +++ b/services/companion/java/com/android/server/companion/virtual/InputController.java @@ -31,6 +31,7 @@ import android.hardware.input.VirtualMouseScrollEvent; import android.hardware.input.VirtualTouchEvent; import android.os.Handler; import android.os.IBinder; +import android.os.IInputConstants; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Slog; @@ -75,7 +76,7 @@ class InputController { @interface PhysType { } - private final Object mLock; + final Object mLock; /* Token -> file descriptor associations. */ @VisibleForTesting @@ -220,6 +221,19 @@ class InputController { } } + /** + * @return the device id for a given token (identifiying a device) + */ + int getInputDeviceId(IBinder token) { + synchronized (mLock) { + final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(token); + if (inputDeviceDescriptor == null) { + throw new IllegalArgumentException("Could not get device id for given token"); + } + return inputDeviceDescriptor.getInputDeviceId(); + } + } + void setShowPointerIcon(boolean visible, int displayId) { mInputManagerInternal.setPointerIconVisible(visible, displayId); } @@ -393,10 +407,22 @@ class InputController { + inputDeviceDescriptor.getCreationOrderNumber()); fout.println(" type: " + inputDeviceDescriptor.getType()); fout.println(" phys: " + inputDeviceDescriptor.getPhys()); + fout.println( + " inputDeviceId: " + inputDeviceDescriptor.getInputDeviceId()); } } } + @VisibleForTesting + void addDeviceForTesting(IBinder deviceToken, int fd, int type, int displayId, + String phys, int inputDeviceId) { + synchronized (mLock) { + mInputDeviceDescriptors.put(deviceToken, + new InputDeviceDescriptor(fd, () -> {}, type, displayId, phys, + inputDeviceId)); + } + } + private static native int nativeOpenUinputDpad(String deviceName, int vendorId, int productId, String phys); private static native int nativeOpenUinputKeyboard(String deviceName, int vendorId, @@ -493,16 +519,20 @@ class InputController { private final @Type int mType; private final int mDisplayId; private final String mPhys; + // The input device id that was associated to the device by the InputReader on device + // creation. + private final int mInputDeviceId; // Monotonically increasing number; devices with lower numbers were created earlier. private final long mCreationOrderNumber; InputDeviceDescriptor(int fd, IBinder.DeathRecipient deathRecipient, @Type int type, - int displayId, String phys) { + int displayId, String phys, int inputDeviceId) { mFd = fd; mDeathRecipient = deathRecipient; mType = type; mDisplayId = displayId; mPhys = phys; + mInputDeviceId = inputDeviceId; mCreationOrderNumber = sNextCreationOrderNumber.getAndIncrement(); } @@ -533,6 +563,10 @@ class InputController { public String getPhys() { return mPhys; } + + public int getInputDeviceId() { + return mInputDeviceId; + } } private final class BinderDeathRecipient implements IBinder.DeathRecipient { @@ -558,6 +592,8 @@ class InputController { private final CountDownLatch mDeviceAddedLatch = new CountDownLatch(1); private final InputManager.InputDeviceListener mListener; + private int mInputDeviceId = IInputConstants.INVALID_INPUT_DEVICE_ID; + WaitForDevice(String deviceName, int vendorId, int productId) { mListener = new InputManager.InputDeviceListener() { @Override @@ -572,6 +608,7 @@ class InputController { if (id.getVendorId() != vendorId || id.getProductId() != productId) { return; } + mInputDeviceId = deviceId; mDeviceAddedLatch.countDown(); } @@ -588,8 +625,13 @@ class InputController { InputManager.getInstance().registerInputDeviceListener(mListener, mHandler); } - /** Note: This must not be called from {@link #mHandler}'s thread. */ - void waitForDeviceCreation() throws DeviceCreationException { + /** + * Note: This must not be called from {@link #mHandler}'s thread. + * @throws DeviceCreationException if the device was not created successfully within the + * timeout. + * @return The id of the created input device. + */ + int waitForDeviceCreation() throws DeviceCreationException { try { if (!mDeviceAddedLatch.await(1, TimeUnit.MINUTES)) { throw new DeviceCreationException( @@ -599,6 +641,12 @@ class InputController { throw new DeviceCreationException( "Interrupted while waiting for virtual device to be created.", e); } + if (mInputDeviceId == IInputConstants.INVALID_INPUT_DEVICE_ID) { + throw new IllegalStateException( + "Virtual input device was created with an invalid " + + "id=" + mInputDeviceId); + } + return mInputDeviceId; } @Override @@ -643,6 +691,8 @@ class InputController { final int fd; final BinderDeathRecipient binderDeathRecipient; + final int inputDeviceId; + setUniqueIdAssociation(displayId, phys); try (WaitForDevice waiter = new WaitForDevice(deviceName, vendorId, productId)) { fd = deviceOpener.get(); @@ -652,7 +702,7 @@ class InputController { } // The fd is valid from here, so ensure that all failures close the fd after this point. try { - waiter.waitForDeviceCreation(); + inputDeviceId = waiter.waitForDeviceCreation(); binderDeathRecipient = new BinderDeathRecipient(deviceToken); try { @@ -672,7 +722,8 @@ class InputController { synchronized (mLock) { mInputDeviceDescriptors.put(deviceToken, - new InputDeviceDescriptor(fd, binderDeathRecipient, type, displayId, phys)); + new InputDeviceDescriptor(fd, binderDeathRecipient, type, displayId, phys, + inputDeviceId)); } } diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 2835b69b3039..5ebbf07526f1 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -498,6 +498,17 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + public int getInputDeviceId(IBinder token) { + final long binderToken = Binder.clearCallingIdentity(); + try { + return mInputController.getInputDeviceId(token); + } finally { + Binder.restoreCallingIdentity(binderToken); + } + } + + + @Override // Binder call public boolean sendDpadKeyEvent(IBinder token, VirtualKeyEvent event) { final long binderToken = Binder.clearCallingIdentity(); try { diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index f7833b0f36fd..2652ebec5255 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -2581,33 +2581,39 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { if (!checkNotifyPermission("notifyBarringInfo()")) { return; } - if (barringInfo == null) { - log("Received null BarringInfo for subId=" + subId + ", phoneId=" + phoneId); - mBarringInfo.set(phoneId, new BarringInfo()); + if (!validatePhoneId(phoneId)) { + loge("Received invalid phoneId for BarringInfo = " + phoneId); return; } synchronized (mRecords) { - if (validatePhoneId(phoneId)) { - mBarringInfo.set(phoneId, barringInfo); - // Barring info is non-null - BarringInfo biNoLocation = barringInfo.createLocationInfoSanitizedCopy(); - if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo); - for (Record r : mRecords) { - if (r.matchTelephonyCallbackEvent( - TelephonyCallback.EVENT_BARRING_INFO_CHANGED) - && idMatch(r, subId, phoneId)) { - try { - if (DBG_LOC) { - log("notifyBarringInfo: mBarringInfo=" - + barringInfo + " r=" + r); - } - r.callback.onBarringInfoChanged( - checkFineLocationAccess(r, Build.VERSION_CODES.BASE) - ? barringInfo : biNoLocation); - } catch (RemoteException ex) { - mRemoveList.add(r.binder); + if (barringInfo == null) { + loge("Received null BarringInfo for subId=" + subId + ", phoneId=" + phoneId); + mBarringInfo.set(phoneId, new BarringInfo()); + return; + } + if (barringInfo.equals(mBarringInfo.get(phoneId))) { + if (VDBG) log("Ignoring duplicate barring info."); + return; + } + mBarringInfo.set(phoneId, barringInfo); + // Barring info is non-null + BarringInfo biNoLocation = barringInfo.createLocationInfoSanitizedCopy(); + if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo); + for (Record r : mRecords) { + if (r.matchTelephonyCallbackEvent( + TelephonyCallback.EVENT_BARRING_INFO_CHANGED) + && idMatch(r, subId, phoneId)) { + try { + if (DBG_LOC) { + log("notifyBarringInfo: mBarringInfo=" + + barringInfo + " r=" + r); } + r.callback.onBarringInfoChanged( + checkFineLocationAccess(r, Build.VERSION_CODES.BASE) + ? barringInfo : biNoLocation); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); } } } diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index 202f47759272..5d46de335781 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -152,6 +152,8 @@ final class UiModeManagerService extends SystemService { // flag set by resource, whether to start dream immediately upon docking even if unlocked. private boolean mStartDreamImmediatelyOnDock = true; + // flag set by resource, whether to disable dreams when ambient mode suppression is enabled. + private boolean mDreamsDisabledByAmbientModeSuppression = false; // flag set by resource, whether to enable Car dock launch when starting car mode. private boolean mEnableCarDockLaunch = true; // flag set by resource, whether to lock UI mode to the default one or not. @@ -364,6 +366,11 @@ final class UiModeManagerService extends SystemService { mStartDreamImmediatelyOnDock = startDreamImmediatelyOnDock; } + @VisibleForTesting + void setDreamsDisabledByAmbientModeSuppression(boolean disabledByAmbientModeSuppression) { + mDreamsDisabledByAmbientModeSuppression = disabledByAmbientModeSuppression; + } + @Override public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) { mCurrentUser = to.getUserIdentifier(); @@ -424,6 +431,8 @@ final class UiModeManagerService extends SystemService { final Resources res = context.getResources(); mStartDreamImmediatelyOnDock = res.getBoolean( com.android.internal.R.bool.config_startDreamImmediatelyOnDock); + mDreamsDisabledByAmbientModeSuppression = res.getBoolean( + com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig); mNightMode = res.getInteger( com.android.internal.R.integer.config_defaultNightMode); mDefaultUiModeType = res.getInteger( @@ -1827,10 +1836,14 @@ final class UiModeManagerService extends SystemService { // Send the new configuration. applyConfigurationExternallyLocked(); + final boolean dreamsSuppressed = mDreamsDisabledByAmbientModeSuppression + && mLocalPowerManager.isAmbientDisplaySuppressed(); + // If we did not start a dock app, then start dreaming if appropriate. - if (category != null && !dockAppStarted && (mStartDreamImmediatelyOnDock - || mWindowManager.isKeyguardShowingAndNotOccluded() - || !mPowerManager.isInteractive())) { + if (category != null && !dockAppStarted && !dreamsSuppressed && ( + mStartDreamImmediatelyOnDock + || mWindowManager.isKeyguardShowingAndNotOccluded() + || !mPowerManager.isInteractive())) { mInjector.startDreamWhenDockedIfAppropriate(getContext()); } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 7a09109e377b..63f81822e867 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -13373,27 +13373,19 @@ public class ActivityManagerService extends IActivityManager.Stub int callingPid; boolean instantApp; synchronized(this) { - if (caller != null) { - callerApp = getRecordForAppLOSP(caller); - if (callerApp == null) { - throw new SecurityException( - "Unable to find app for caller " + caller - + " (pid=" + Binder.getCallingPid() - + ") when registering receiver " + receiver); - } - if (callerApp.info.uid != SYSTEM_UID - && !callerApp.getPkgList().containsKey(callerPackage) - && !"android".equals(callerPackage)) { - throw new SecurityException("Given caller package " + callerPackage - + " is not running in process " + callerApp); - } - callingUid = callerApp.info.uid; - callingPid = callerApp.getPid(); - } else { - callerPackage = null; - callingUid = Binder.getCallingUid(); - callingPid = Binder.getCallingPid(); + callerApp = getRecordForAppLOSP(caller); + if (callerApp == null) { + Slog.w(TAG, "registerReceiverWithFeature: no app for " + caller); + return null; + } + if (callerApp.info.uid != SYSTEM_UID + && !callerApp.getPkgList().containsKey(callerPackage) + && !"android".equals(callerPackage)) { + throw new SecurityException("Given caller package " + callerPackage + + " is not running in process " + callerApp); } + callingUid = callerApp.info.uid; + callingPid = callerApp.getPid(); instantApp = isInstantApp(callerApp, callerPackage, callingUid); userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, true, @@ -14700,13 +14692,14 @@ public class ActivityManagerService extends IActivityManager.Stub // Non-system callers can't declare that a broadcast is alarm-related. // The PendingIntent invocation case is handled in PendingIntentRecord. if (bOptions != null && callingUid != SYSTEM_UID) { - if (bOptions.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) { + if (bOptions.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST) + || bOptions.containsKey(BroadcastOptions.KEY_INTERACTIVE_BROADCAST)) { if (DEBUG_BROADCAST) { Slog.w(TAG, "Non-system caller " + callingUid - + " may not flag broadcast as alarm-related"); + + " may not flag broadcast as alarm or interactive"); } throw new SecurityException( - "Non-system callers may not flag broadcasts as alarm-related"); + "Non-system callers may not flag broadcasts as alarm or interactive"); } } diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java index a4a1c2f0d87c..28a81e6ee145 100644 --- a/services/core/java/com/android/server/am/BroadcastConstants.java +++ b/services/core/java/com/android/server/am/BroadcastConstants.java @@ -167,7 +167,7 @@ public class BroadcastConstants { */ public long DELAY_NORMAL_MILLIS = DEFAULT_DELAY_NORMAL_MILLIS; private static final String KEY_DELAY_NORMAL_MILLIS = "bcast_delay_normal_millis"; - private static final long DEFAULT_DELAY_NORMAL_MILLIS = 10_000 * Build.HW_TIMEOUT_MULTIPLIER; + private static final long DEFAULT_DELAY_NORMAL_MILLIS = 1_000; /** * For {@link BroadcastQueueModernImpl}: Delay to apply to broadcasts @@ -175,7 +175,7 @@ public class BroadcastConstants { */ public long DELAY_CACHED_MILLIS = DEFAULT_DELAY_CACHED_MILLIS; private static final String KEY_DELAY_CACHED_MILLIS = "bcast_delay_cached_millis"; - private static final long DEFAULT_DELAY_CACHED_MILLIS = 30_000 * Build.HW_TIMEOUT_MULTIPLIER; + private static final long DEFAULT_DELAY_CACHED_MILLIS = 10_000; /** * For {@link BroadcastQueueModernImpl}: Maximum number of complete diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index 0d6ac1d57387..868c3ae6da50 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -103,6 +103,13 @@ class BroadcastProcessQueue { private final ArrayDeque<SomeArgs> mPending = new ArrayDeque<>(); /** + * Ordered collection of "urgent" broadcasts that are waiting to be + * dispatched to this process, in the same representation as + * {@link #mPending}. + */ + private final ArrayDeque<SomeArgs> mPendingUrgent = new ArrayDeque<>(); + + /** * Broadcast actively being dispatched to this process. */ private @Nullable BroadcastRecord mActive; @@ -140,12 +147,16 @@ class BroadcastProcessQueue { private int mCountOrdered; private int mCountAlarm; private int mCountPrioritized; + private int mCountInteractive; + private int mCountResultTo; + private int mCountInstrumented; private @UptimeMillisLong long mRunnableAt = Long.MAX_VALUE; private @Reason int mRunnableAtReason = REASON_EMPTY; private boolean mRunnableAtInvalidated; private boolean mProcessCached; + private boolean mProcessInstrumented; private String mCachedToString; private String mCachedToShortString; @@ -172,40 +183,65 @@ class BroadcastProcessQueue { */ public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex, int blockedUntilTerminalCount) { - // If caller wants to replace, walk backwards looking for any matches if (record.isReplacePending()) { - final Iterator<SomeArgs> it = mPending.descendingIterator(); - final Object receiver = record.receivers.get(recordIndex); - while (it.hasNext()) { - final SomeArgs args = it.next(); - final BroadcastRecord testRecord = (BroadcastRecord) args.arg1; - final Object testReceiver = testRecord.receivers.get(args.argi1); - if ((record.callingUid == testRecord.callingUid) - && (record.userId == testRecord.userId) - && record.intent.filterEquals(testRecord.intent) - && isReceiverEquals(receiver, testReceiver)) { - // Exact match found; perform in-place swap - args.arg1 = record; - args.argi1 = recordIndex; - args.argi2 = blockedUntilTerminalCount; - onBroadcastDequeued(testRecord); - onBroadcastEnqueued(record); - return; - } + boolean didReplace = replaceBroadcastInQueue(mPending, + record, recordIndex, blockedUntilTerminalCount) + || replaceBroadcastInQueue(mPendingUrgent, + record, recordIndex, blockedUntilTerminalCount); + if (didReplace) { + return; } } // Caller isn't interested in replacing, or we didn't find any pending // item to replace above, so enqueue as a new broadcast - SomeArgs args = SomeArgs.obtain(); - args.arg1 = record; - args.argi1 = recordIndex; - args.argi2 = blockedUntilTerminalCount; - mPending.addLast(args); + SomeArgs newBroadcastArgs = SomeArgs.obtain(); + newBroadcastArgs.arg1 = record; + newBroadcastArgs.argi1 = recordIndex; + newBroadcastArgs.argi2 = blockedUntilTerminalCount; + + // Cross-broadcast prioritization policy: some broadcasts might warrant being + // issued ahead of others that are already pending, for example if this new + // broadcast is in a different delivery class or is tied to a direct user interaction + // with implicit responsiveness expectations. + final ArrayDeque<SomeArgs> queue = record.isUrgent() ? mPendingUrgent : mPending; + queue.addLast(newBroadcastArgs); onBroadcastEnqueued(record); } /** + * Searches from newest to oldest, and at the first matching pending broadcast + * it finds, replaces it in-place and returns -- does not attempt to handle + * "duplicate" broadcasts in the queue. + * <p> + * @return {@code true} if it found and replaced an existing record in the queue; + * {@code false} otherwise. + */ + private boolean replaceBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue, + @NonNull BroadcastRecord record, int recordIndex, int blockedUntilTerminalCount) { + final Iterator<SomeArgs> it = queue.descendingIterator(); + final Object receiver = record.receivers.get(recordIndex); + while (it.hasNext()) { + final SomeArgs args = it.next(); + final BroadcastRecord testRecord = (BroadcastRecord) args.arg1; + final Object testReceiver = testRecord.receivers.get(args.argi1); + if ((record.callingUid == testRecord.callingUid) + && (record.userId == testRecord.userId) + && record.intent.filterEquals(testRecord.intent) + && isReceiverEquals(receiver, testReceiver)) { + // Exact match found; perform in-place swap + args.arg1 = record; + args.argi1 = recordIndex; + args.argi2 = blockedUntilTerminalCount; + onBroadcastDequeued(testRecord); + onBroadcastEnqueued(record); + return true; + } + } + return false; + } + + /** * Functional interface that tests a {@link BroadcastRecord} that has been * previously enqueued in {@link BroadcastProcessQueue}. */ @@ -233,8 +269,18 @@ class BroadcastProcessQueue { */ public boolean forEachMatchingBroadcast(@NonNull BroadcastPredicate predicate, @NonNull BroadcastConsumer consumer, boolean andRemove) { + boolean didSomething = forEachMatchingBroadcastInQueue(mPending, + predicate, consumer, andRemove); + didSomething |= forEachMatchingBroadcastInQueue(mPendingUrgent, + predicate, consumer, andRemove); + return didSomething; + } + + private boolean forEachMatchingBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue, + @NonNull BroadcastPredicate predicate, @NonNull BroadcastConsumer consumer, + boolean andRemove) { boolean didSomething = false; - final Iterator<SomeArgs> it = mPending.iterator(); + final Iterator<SomeArgs> it = queue.iterator(); while (it.hasNext()) { final SomeArgs args = it.next(); final BroadcastRecord record = (BroadcastRecord) args.arg1; @@ -255,6 +301,18 @@ class BroadcastProcessQueue { } /** + * Update the actively running "warm" process for this process. + */ + public void setProcess(@Nullable ProcessRecord app) { + this.app = app; + if (app != null) { + setProcessInstrumented(app.getActiveInstrumentation() != null); + } else { + setProcessInstrumented(false); + } + } + + /** * Update if this process is in the "cached" state, typically signaling that * broadcast dispatch should be paused or delayed. */ @@ -266,6 +324,18 @@ class BroadcastProcessQueue { } /** + * Update if this process is in the "instrumented" state, typically + * signaling that broadcast dispatch should bypass all pauses or delays, to + * avoid holding up test suites. + */ + public void setProcessInstrumented(boolean instrumented) { + if (mProcessInstrumented != instrumented) { + mProcessInstrumented = instrumented; + invalidateRunnableAt(); + } + } + + /** * Return if we know of an actively running "warm" process for this queue. */ public boolean isProcessWarm() { @@ -273,13 +343,12 @@ class BroadcastProcessQueue { } public int getPreferredSchedulingGroupLocked() { - if (mCountForeground > 0 || mCountOrdered > 0 || mCountAlarm > 0) { - // We have an important broadcast somewhere down the queue, so + if (mCountForeground > 0) { + // We have a foreground broadcast somewhere down the queue, so // boost priority until we drain them all return ProcessList.SCHED_GROUP_DEFAULT; - } else if ((mActive != null) - && (mActive.isForeground() || mActive.ordered || mActive.alarm)) { - // We have an important broadcast right now, so boost priority + } else if ((mActive != null) && mActive.isForeground()) { + // We have a foreground broadcast right now, so boost priority return ProcessList.SCHED_GROUP_DEFAULT; } else if (!isIdle()) { return ProcessList.SCHED_GROUP_BACKGROUND; @@ -309,7 +378,7 @@ class BroadcastProcessQueue { */ public void makeActiveNextPending() { // TODO: what if the next broadcast isn't runnable yet? - final SomeArgs next = mPending.removeFirst(); + final SomeArgs next = removeNextBroadcast(); mActive = (BroadcastRecord) next.arg1; mActiveIndex = next.argi1; mActiveBlockedUntilTerminalCount = next.argi2; @@ -347,6 +416,15 @@ class BroadcastProcessQueue { if (record.prioritized) { mCountPrioritized++; } + if (record.interactive) { + mCountInteractive++; + } + if (record.resultTo != null) { + mCountResultTo++; + } + if (record.callerInstrumented) { + mCountInstrumented++; + } invalidateRunnableAt(); } @@ -366,6 +444,15 @@ class BroadcastProcessQueue { if (record.prioritized) { mCountPrioritized--; } + if (record.interactive) { + mCountInteractive--; + } + if (record.resultTo != null) { + mCountResultTo--; + } + if (record.callerInstrumented) { + mCountInstrumented--; + } invalidateRunnableAt(); } @@ -413,7 +500,7 @@ class BroadcastProcessQueue { } public boolean isEmpty() { - return mPending.isEmpty(); + return mPending.isEmpty() && mPendingUrgent.isEmpty(); } public boolean isActive() { @@ -421,6 +508,38 @@ class BroadcastProcessQueue { } /** + * Will thrown an exception if there are no pending broadcasts; relies on + * {@link #isEmpty()} being false. + */ + SomeArgs removeNextBroadcast() { + ArrayDeque<SomeArgs> queue = queueForNextBroadcast(); + return queue.removeFirst(); + } + + @Nullable ArrayDeque<SomeArgs> queueForNextBroadcast() { + if (!mPendingUrgent.isEmpty()) { + return mPendingUrgent; + } else if (!mPending.isEmpty()) { + return mPending; + } + return null; + } + + /** + * Returns null if there are no pending broadcasts + */ + @Nullable SomeArgs peekNextBroadcast() { + ArrayDeque<SomeArgs> queue = queueForNextBroadcast(); + return (queue != null) ? queue.peekFirst() : null; + } + + @VisibleForTesting + @Nullable BroadcastRecord peekNextBroadcastRecord() { + ArrayDeque<SomeArgs> queue = queueForNextBroadcast(); + return (queue != null) ? (BroadcastRecord) queue.peekFirst().arg1 : null; + } + + /** * Quickly determine if this queue has broadcasts that are still waiting to * be delivered at some point in the future. */ @@ -437,11 +556,13 @@ class BroadcastProcessQueue { return mActive.enqueueTime > barrierTime; } final SomeArgs next = mPending.peekFirst(); - if (next != null) { - return ((BroadcastRecord) next.arg1).enqueueTime > barrierTime; - } - // Nothing running or runnable means we're past the barrier - return true; + final SomeArgs nextUrgent = mPendingUrgent.peekFirst(); + // Empty queue is past any barrier + final boolean nextLater = next == null + || ((BroadcastRecord) next.arg1).enqueueTime > barrierTime; + final boolean nextUrgentLater = nextUrgent == null + || ((BroadcastRecord) nextUrgent.arg1).enqueueTime > barrierTime; + return nextLater && nextUrgentLater; } public boolean isRunnable() { @@ -477,25 +598,33 @@ class BroadcastProcessQueue { } static final int REASON_EMPTY = 0; - static final int REASON_CONTAINS_FOREGROUND = 1; - static final int REASON_CONTAINS_ORDERED = 2; - static final int REASON_CONTAINS_ALARM = 3; - static final int REASON_CONTAINS_PRIORITIZED = 4; - static final int REASON_CACHED = 5; - static final int REASON_NORMAL = 6; - static final int REASON_MAX_PENDING = 7; - static final int REASON_BLOCKED = 8; + static final int REASON_CACHED = 1; + static final int REASON_NORMAL = 2; + static final int REASON_MAX_PENDING = 3; + static final int REASON_BLOCKED = 4; + static final int REASON_INSTRUMENTED = 5; + static final int REASON_CONTAINS_FOREGROUND = 10; + static final int REASON_CONTAINS_ORDERED = 11; + static final int REASON_CONTAINS_ALARM = 12; + static final int REASON_CONTAINS_PRIORITIZED = 13; + static final int REASON_CONTAINS_INTERACTIVE = 14; + static final int REASON_CONTAINS_RESULT_TO = 15; + static final int REASON_CONTAINS_INSTRUMENTED = 16; @IntDef(flag = false, prefix = { "REASON_" }, value = { REASON_EMPTY, - REASON_CONTAINS_FOREGROUND, - REASON_CONTAINS_ORDERED, - REASON_CONTAINS_ALARM, - REASON_CONTAINS_PRIORITIZED, REASON_CACHED, REASON_NORMAL, REASON_MAX_PENDING, REASON_BLOCKED, + REASON_INSTRUMENTED, + REASON_CONTAINS_FOREGROUND, + REASON_CONTAINS_ORDERED, + REASON_CONTAINS_ALARM, + REASON_CONTAINS_PRIORITIZED, + REASON_CONTAINS_INTERACTIVE, + REASON_CONTAINS_RESULT_TO, + REASON_CONTAINS_INSTRUMENTED, }) @Retention(RetentionPolicy.SOURCE) public @interface Reason {} @@ -503,14 +632,18 @@ class BroadcastProcessQueue { static @NonNull String reasonToString(@Reason int reason) { switch (reason) { case REASON_EMPTY: return "EMPTY"; - case REASON_CONTAINS_FOREGROUND: return "CONTAINS_FOREGROUND"; - case REASON_CONTAINS_ORDERED: return "CONTAINS_ORDERED"; - case REASON_CONTAINS_ALARM: return "CONTAINS_ALARM"; - case REASON_CONTAINS_PRIORITIZED: return "CONTAINS_PRIORITIZED"; case REASON_CACHED: return "CACHED"; case REASON_NORMAL: return "NORMAL"; case REASON_MAX_PENDING: return "MAX_PENDING"; case REASON_BLOCKED: return "BLOCKED"; + case REASON_INSTRUMENTED: return "INSTRUMENTED"; + case REASON_CONTAINS_FOREGROUND: return "CONTAINS_FOREGROUND"; + case REASON_CONTAINS_ORDERED: return "CONTAINS_ORDERED"; + case REASON_CONTAINS_ALARM: return "CONTAINS_ALARM"; + case REASON_CONTAINS_PRIORITIZED: return "CONTAINS_PRIORITIZED"; + case REASON_CONTAINS_INTERACTIVE: return "CONTAINS_INTERACTIVE"; + case REASON_CONTAINS_RESULT_TO: return "CONTAINS_RESULT_TO"; + case REASON_CONTAINS_INSTRUMENTED: return "CONTAINS_INSTRUMENTED"; default: return Integer.toString(reason); } } @@ -519,7 +652,7 @@ class BroadcastProcessQueue { * Update {@link #getRunnableAt()} if it's currently invalidated. */ private void updateRunnableAt() { - final SomeArgs next = mPending.peekFirst(); + final SomeArgs next = peekNextBroadcast(); if (next != null) { final BroadcastRecord r = (BroadcastRecord) next.arg1; final int index = next.argi1; @@ -537,7 +670,7 @@ class BroadcastProcessQueue { // If we have too many broadcasts pending, bypass any delays that // might have been applied above to aid draining - if (mPending.size() >= constants.MAX_PENDING_BROADCASTS) { + if (mPending.size() + mPendingUrgent.size() >= constants.MAX_PENDING_BROADCASTS) { mRunnableAt = runnableAt; mRunnableAtReason = REASON_MAX_PENDING; return; @@ -555,6 +688,18 @@ class BroadcastProcessQueue { } else if (mCountPrioritized > 0) { mRunnableAt = runnableAt; mRunnableAtReason = REASON_CONTAINS_PRIORITIZED; + } else if (mCountInteractive > 0) { + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_CONTAINS_INTERACTIVE; + } else if (mCountResultTo > 0) { + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_CONTAINS_RESULT_TO; + } else if (mCountInstrumented > 0) { + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_CONTAINS_INSTRUMENTED; + } else if (mProcessInstrumented) { + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_INSTRUMENTED; } else if (mProcessCached) { mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS; mRunnableAtReason = REASON_CACHED; @@ -574,8 +719,8 @@ class BroadcastProcessQueue { */ public void checkHealthLocked() { if (mRunnableAtReason == REASON_BLOCKED) { - final SomeArgs next = mPending.peekFirst(); - Objects.requireNonNull(next, "peekFirst"); + final SomeArgs next = peekNextBroadcast(); + Objects.requireNonNull(next, "peekNextBroadcast"); // If blocked more than 10 minutes, we're likely wedged final BroadcastRecord r = (BroadcastRecord) next.arg1; diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index 1e1ebeba5c23..db3ef3d51b16 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -44,6 +44,7 @@ import android.annotation.Nullable; import android.annotation.UptimeMillisLong; import android.app.Activity; import android.app.ActivityManager; +import android.app.BroadcastOptions; import android.app.IApplicationThread; import android.app.RemoteServiceException.CannotDeliverBroadcastException; import android.app.UidObserver; @@ -440,7 +441,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // relevant per-process queue final BroadcastProcessQueue queue = getProcessQueue(app); if (queue != null) { - queue.app = app; + queue.setProcess(app); } boolean didSomething = false; @@ -477,7 +478,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // relevant per-process queue final BroadcastProcessQueue queue = getProcessQueue(app); if (queue != null) { - queue.app = null; + queue.setProcess(null); } if ((mRunningColdStart != null) && (mRunningColdStart == queue)) { @@ -534,6 +535,17 @@ class BroadcastQueueModernImpl extends BroadcastQueue { }, mBroadcastConsumerSkipAndCanceled, true); } + final int policy = (r.options != null) + ? r.options.getDeliveryGroupPolicy() : BroadcastOptions.DELIVERY_GROUP_POLICY_ALL; + if (policy == BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) { + forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> { + // We only allow caller to remove broadcasts they enqueued + return (r.callingUid == testRecord.callingUid) + && (r.userId == testRecord.userId) + && r.matchesDeliveryGroup(testRecord); + }, mBroadcastConsumerSkipAndCanceled, true); + } + if (r.isReplacePending()) { // Leave the skipped broadcasts intact in queue, so that we can // replace them at their current position during enqueue below @@ -804,19 +816,21 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } final BroadcastRecord r = queue.getActive(); - r.resultCode = resultCode; - r.resultData = resultData; - r.resultExtras = resultExtras; - if (!r.isNoAbort()) { - r.resultAbort = resultAbort; - } - - // When the caller aborted an ordered broadcast, we mark all remaining - // receivers as skipped - if (r.ordered && r.resultAbort) { - for (int i = r.terminalCount + 1; i < r.receivers.size(); i++) { - setDeliveryState(null, null, r, i, r.receivers.get(i), - BroadcastRecord.DELIVERY_SKIPPED); + if (r.ordered) { + r.resultCode = resultCode; + r.resultData = resultData; + r.resultExtras = resultExtras; + if (!r.isNoAbort()) { + r.resultAbort = resultAbort; + } + + // When the caller aborted an ordered broadcast, we mark all + // remaining receivers as skipped + if (r.resultAbort) { + for (int i = r.terminalCount + 1; i < r.receivers.size(); i++) { + setDeliveryState(null, null, r, i, r.receivers.get(i), + BroadcastRecord.DELIVERY_SKIPPED); + } } } @@ -913,7 +927,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { notifyFinishReceiver(queue, r, index, receiver); // When entire ordered broadcast finished, deliver final result - if (r.ordered && (r.terminalCount == r.receivers.size())) { + final boolean recordFinished = (r.terminalCount == r.receivers.size()); + if (recordFinished) { scheduleResultTo(r); } @@ -1205,7 +1220,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { private void updateWarmProcess(@NonNull BroadcastProcessQueue queue) { if (!queue.isProcessWarm()) { - queue.app = mService.getProcessRecordLocked(queue.processName, queue.uid); + queue.setProcess(mService.getProcessRecordLocked(queue.processName, queue.uid)); } } diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index 2d825955ed6c..d7dc8b80931b 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -78,11 +78,13 @@ final class BroadcastRecord extends Binder { final int callingPid; // the pid of who sent this final int callingUid; // the uid of who sent this final boolean callerInstantApp; // caller is an Instant App? + final boolean callerInstrumented; // caller is being instrumented final boolean ordered; // serialize the send to receivers? final boolean sticky; // originated from existing sticky data? final boolean alarm; // originated from an alarm triggering? final boolean pushMessage; // originated from a push message? final boolean pushMessageOverQuota; // originated from a push message which was over quota? + final boolean interactive; // originated from user interaction? final boolean initialSticky; // initial broadcast from register to sticky? final boolean prioritized; // contains more than one priority tranche final int userId; // user id this broadcast was for @@ -364,6 +366,8 @@ final class BroadcastRecord extends Binder { callingPid = _callingPid; callingUid = _callingUid; callerInstantApp = _callerInstantApp; + callerInstrumented = (_callerApp != null) + ? (_callerApp.getActiveInstrumentation() != null) : false; resolvedType = _resolvedType; requiredPermissions = _requiredPermissions; excludedPermissions = _excludedPermissions; @@ -392,6 +396,7 @@ final class BroadcastRecord extends Binder { alarm = options != null && options.isAlarmBroadcast(); pushMessage = options != null && options.isPushMessagingBroadcast(); pushMessageOverQuota = options != null && options.isPushMessagingOverQuotaBroadcast(); + interactive = options != null && options.isInteractiveBroadcast(); this.filterExtrasForReceiver = filterExtrasForReceiver; } @@ -409,6 +414,7 @@ final class BroadcastRecord extends Binder { callingPid = from.callingPid; callingUid = from.callingUid; callerInstantApp = from.callerInstantApp; + callerInstrumented = from.callerInstrumented; ordered = from.ordered; sticky = from.sticky; initialSticky = from.initialSticky; @@ -450,6 +456,7 @@ final class BroadcastRecord extends Binder { alarm = from.alarm; pushMessage = from.pushMessage; pushMessageOverQuota = from.pushMessageOverQuota; + interactive = from.interactive; filterExtrasForReceiver = from.filterExtrasForReceiver; } @@ -611,6 +618,18 @@ final class BroadcastRecord extends Binder { return (intent.getFlags() & Intent.FLAG_RECEIVER_NO_ABORT) != 0; } + /** + * Core policy determination about this broadcast's delivery prioritization + */ + boolean isUrgent() { + // TODO: flags for controlling policy + // TODO: migrate alarm-prioritization flag to BroadcastConstants + return (isForeground() + || interactive + || alarm) + && receivers.size() == 1; + } + @NonNull String getHostingRecordTriggerType() { if (alarm) { return HostingRecord.TRIGGER_TYPE_ALARM; @@ -796,6 +815,16 @@ final class BroadcastRecord extends Binder { } } + public boolean matchesDeliveryGroup(@NonNull BroadcastRecord other) { + final String key = (options != null) ? options.getDeliveryGroupKey() : null; + final String otherKey = (other.options != null) + ? other.options.getDeliveryGroupKey() : null; + if (key == null && otherKey == null) { + return intent.filterEquals(other.intent); + } + return Objects.equals(key, otherKey); + } + @Override public String toString() { if (mCachedToString == null) { diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 975619feaea0..740efbc658ba 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -443,13 +443,14 @@ public final class PendingIntentRecord extends IIntentSender.Stub { // invocation side effects such as allowlisting. if (options != null && callingUid != Process.SYSTEM_UID && key.type == ActivityManager.INTENT_SENDER_BROADCAST) { - if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) { + if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST) + || options.containsKey(BroadcastOptions.KEY_INTERACTIVE_BROADCAST)) { if (DEBUG_BROADCAST_LIGHT) { Slog.w(TAG, "Non-system caller " + callingUid - + " may not flag broadcast as alarm-related"); + + " may not flag broadcast as alarm or interactive"); } throw new SecurityException( - "Non-system callers may not flag broadcasts as alarm-related"); + "Non-system callers may not flag broadcasts as alarm or interactive"); } } diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 216a48ec699c..3fa41c0f0420 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -16,7 +16,6 @@ package com.android.server.am; -import static android.Manifest.permission.CREATE_USERS; import static android.Manifest.permission.INTERACT_ACROSS_PROFILES; import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; @@ -1482,7 +1481,7 @@ class UserController implements Handler.Callback { // defined boolean startUserOnSecondaryDisplay(@UserIdInt int userId, int displayId) { checkCallingHasOneOfThosePermissions("startUserOnSecondaryDisplay", - MANAGE_USERS, CREATE_USERS); + MANAGE_USERS, INTERACT_ACROSS_USERS); // DEFAULT_DISPLAY is used for the current foreground user only Preconditions.checkArgument(displayId != Display.DEFAULT_DISPLAY, diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java index dec1b559556a..5bc9d2341535 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java @@ -54,7 +54,7 @@ public class AuthSessionCoordinator implements AuthSessionListener { private AuthResultCoordinator mAuthResultCoordinator; public AuthSessionCoordinator() { - this(SystemClock.currentNetworkTimeClock()); + this(SystemClock.elapsedRealtimeClock()); } @VisibleForTesting diff --git a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java index 1370fd83f6a8..da7781add8c6 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java +++ b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java @@ -21,6 +21,7 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.hardware.biometrics.BiometricConstants; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; @@ -293,4 +294,30 @@ public abstract class BaseClientMonitor implements IBinder.DeathRecipient { + ", requestId=" + getRequestId() + ", userId=" + getTargetUserId() + "}"; } + + /** + * Cancels this ClientMonitor + */ + public void cancel() { + cancelWithoutStarting(mCallback); + } + + /** + * Cancels this ClientMonitor without starting + * @param callback + */ + public void cancelWithoutStarting(@NonNull ClientMonitorCallback callback) { + Slog.d(TAG, "cancelWithoutStarting: " + this); + + final int errorCode = BiometricConstants.BIOMETRIC_ERROR_CANCELED; + try { + ClientMonitorCallbackConverter listener = getListener(); + if (listener != null) { + listener.onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */); + } + } catch (RemoteException e) { + Slog.w(TAG, "Failed to invoke sendError", e); + } + callback.onClientFinished(this, true /* success */); + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java index 9317c4ec12b5..fb978b2ba4b9 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java @@ -543,4 +543,37 @@ public class BiometricScheduler { mPendingOperations.clear(); mCurrentOperation = null; } + + /** + * Marks all pending operations as canceling and cancels the current + * operation. + */ + private void clearScheduler() { + if (mCurrentOperation == null) { + return; + } + for (BiometricSchedulerOperation pendingOperation : mPendingOperations) { + Slog.d(getTag(), "[Watchdog cancelling pending] " + + pendingOperation.getClientMonitor()); + pendingOperation.markCanceling(); + } + Slog.d(getTag(), "[Watchdog cancelling current] " + + mCurrentOperation.getClientMonitor()); + mCurrentOperation.cancel(mHandler, getInternalCallback()); + } + + /** + * Start the timeout for the watchdog. + */ + public void startWatchdog() { + if (mCurrentOperation == null) { + return; + } + final BiometricSchedulerOperation mOperation = mCurrentOperation; + mHandler.postDelayed(() -> { + if (mOperation == mCurrentOperation) { + clearScheduler(); + } + }, 10000); + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java index ef2931ff5850..dacec38b0e7e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java @@ -267,7 +267,7 @@ public class BiometricSchedulerOperation { /** Flags this operation as canceled, if possible, but does not cancel it until started. */ public boolean markCanceling() { - if (mState == STATE_WAITING_IN_QUEUE && isInterruptable()) { + if (mState == STATE_WAITING_IN_QUEUE) { mState = STATE_WAITING_IN_QUEUE_CANCELING; return true; } @@ -287,10 +287,6 @@ public class BiometricSchedulerOperation { } final int currentState = mState; - if (!isInterruptable()) { - Slog.w(TAG, "Cannot cancel - operation not interruptable: " + this); - return; - } if (currentState == STATE_STARTED_CANCELING) { Slog.w(TAG, "Cannot cancel - already invoked for operation: " + this); return; @@ -301,10 +297,10 @@ public class BiometricSchedulerOperation { || currentState == STATE_WAITING_IN_QUEUE_CANCELING || currentState == STATE_WAITING_FOR_COOKIE) { Slog.d(TAG, "[Cancelling] Current client (without start): " + mClientMonitor); - ((Interruptable) mClientMonitor).cancelWithoutStarting(getWrappedCallback(callback)); + mClientMonitor.cancelWithoutStarting(getWrappedCallback(callback)); } else { Slog.d(TAG, "[Cancelling] Current client: " + mClientMonitor); - ((Interruptable) mClientMonitor).cancel(); + mClientMonitor.cancel(); } // forcibly finish this client if the HAL does not acknowledge within the timeout diff --git a/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java b/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java index d9bd04d3f1c8..6605d49ece9b 100644 --- a/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java +++ b/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java @@ -22,7 +22,6 @@ import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMET import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK; import android.hardware.biometrics.BiometricManager; -import android.os.SystemClock; import android.util.Slog; import java.time.Clock; @@ -43,10 +42,6 @@ class MultiBiometricLockoutState { private final Map<Integer, Map<Integer, AuthenticatorState>> mCanUserAuthenticate; private final Clock mClock; - MultiBiometricLockoutState() { - this(SystemClock.currentNetworkTimeClock()); - } - MultiBiometricLockoutState(Clock clock) { mCanUserAuthenticate = new HashMap<>(); mClock = clock; diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java index 271bce9890c6..2761ec04aa7e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java @@ -183,6 +183,18 @@ public class FaceService extends SystemService { receiver, opPackageName, disabledFeatures, previewSurface, debugConsent); } + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) + @Override + public void scheduleWatchdog() { + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); + if (provider == null) { + Slog.w(TAG, "Null provider for scheduling watchdog"); + return; + } + + provider.second.scheduleWatchdog(provider.first); + } + @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_BIOMETRIC) @Override // Binder call public long enrollRemotely(int userId, final IBinder token, final byte[] hardwareAuthToken, diff --git a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java index 4efaedbd5530..85f95cec8377 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java @@ -128,4 +128,10 @@ public interface ServiceProvider extends BiometricServiceProvider<FaceSensorProp @NonNull String opPackageName); void dumpHal(int sensorId, @NonNull FileDescriptor fd, @NonNull String[] args); + + /** + * Schedules watchdog for canceling hung operations + * @param sensorId sensor ID of the associated operation + */ + default void scheduleWatchdog(int sensorId) {} } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java index b60f9d80d425..c12994c993e6 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -52,6 +52,7 @@ import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthenticationClient; import com.android.server.biometrics.sensors.BaseClientMonitor; +import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; import com.android.server.biometrics.sensors.InvalidationRequesterClient; @@ -661,4 +662,14 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { void setTestHalEnabled(boolean enabled) { mTestHalEnabled = enabled; } + + @Override + public void scheduleWatchdog(int sensorId) { + Slog.d(getTag(), "Starting watchdog for face"); + final BiometricScheduler biometricScheduler = mSensors.get(sensorId).getScheduler(); + if (biometricScheduler == null) { + return; + } + biometricScheduler.startWatchdog(); + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java index 7e2742edd47a..b0dc28ddce96 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java @@ -879,6 +879,18 @@ public class FingerprintService extends SystemService { provider.onPowerPressed(); } } + + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) + @Override + public void scheduleWatchdog() { + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); + if (provider == null) { + Slog.w(TAG, "Null provider for scheduling watchdog"); + return; + } + + provider.second.scheduleWatchdog(provider.first); + } }; public FingerprintService(Context context) { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java index 9075e7ec2080..0c29f5615b4c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java @@ -140,4 +140,10 @@ public interface ServiceProvider extends @NonNull ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback, @NonNull String opPackageName); + + /** + * Schedules watchdog for canceling hung operations + * @param sensorId sensor ID of the associated operation + */ + default void scheduleWatchdog(int sensorId) {} } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index f599acac7ed4..2e5663db57b5 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -16,6 +16,7 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; +import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR; import android.annotation.NonNull; @@ -59,6 +60,7 @@ import com.android.server.biometrics.sensors.SensorOverlays; import com.android.server.biometrics.sensors.fingerprint.PowerPressHandler; import com.android.server.biometrics.sensors.fingerprint.Udfps; +import java.time.Clock; import java.util.ArrayList; import java.util.function.Supplier; @@ -92,7 +94,9 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> private long mWaitForAuthKeyguard; private long mWaitForAuthBp; private long mIgnoreAuthFor; + private long mSideFpsLastAcquireStartTime; private Runnable mAuthSuccessRunnable; + private final Clock mClock; FingerprintAuthenticationClient( @NonNull Context context, @@ -117,7 +121,8 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> boolean allowBackgroundAuthentication, @NonNull FingerprintSensorPropertiesInternal sensorProps, @NonNull Handler handler, - @Authenticators.Types int biometricStrength) { + @Authenticators.Types int biometricStrength, + @NonNull Clock clock) { super( context, lazyDaemon, @@ -161,6 +166,8 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage); mBiometricStrength = biometricStrength; mAuthSessionCoordinator = biometricContext.getAuthSessionCoordinator(); + mSideFpsLastAcquireStartTime = -1; + mClock = clock; if (mSensorProps.isAnySidefpsType()) { if (Build.isDebuggable()) { @@ -246,8 +253,14 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> return; } delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp; - Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps waiting for power for: " - + delay + "ms"); + + if (mSideFpsLastAcquireStartTime != -1) { + delay = Math.max(0, + delay - (mClock.millis() - mSideFpsLastAcquireStartTime)); + } + + Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps " + + "waiting for power until: " + delay + "ms"); } if (mHandler.hasMessages(MESSAGE_FINGER_UP)) { @@ -271,13 +284,15 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> mSensorOverlays.ifUdfps(controller -> controller.onAcquired(getSensorId(), acquiredInfo)); super.onAcquired(acquiredInfo, vendorCode); if (mSensorProps.isAnySidefpsType()) { + if (acquiredInfo == FINGERPRINT_ACQUIRED_START) { + mSideFpsLastAcquireStartTime = mClock.millis(); + } final boolean shouldLookForVendor = mSkipWaitForPowerAcquireMessage == FINGERPRINT_ACQUIRED_VENDOR; final boolean acquireMessageMatch = acquiredInfo == mSkipWaitForPowerAcquireMessage; final boolean vendorMessageMatch = vendorCode == mSkipWaitForPowerVendorAcquireMessage; final boolean ignorePowerPress = - (acquireMessageMatch && !shouldLookForVendor) || (shouldLookForVendor - && acquireMessageMatch && vendorMessageMatch); + acquireMessageMatch && (!shouldLookForVendor || vendorMessageMatch); if (ignorePowerPress) { Slog.d(TAG, "(sideFPS) onFingerUp"); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index 774aff1dd72c..17ba07f2c2bd 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -47,6 +47,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemClock; import android.os.UserManager; import android.util.Slog; import android.util.SparseArray; @@ -58,6 +59,7 @@ import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthenticationClient; import com.android.server.biometrics.sensors.BaseClientMonitor; +import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; @@ -449,7 +451,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mTaskStackListener, mSensors.get(sensorId).getLockoutCache(), mUdfpsOverlayController, mSidefpsController, allowBackgroundAuthentication, mSensors.get(sensorId).getSensorProperties(), mHandler, - Utils.getCurrentStrength(sensorId)); + Utils.getCurrentStrength(sensorId), + SystemClock.elapsedRealtimeClock()); scheduleForSensor(sensorId, client, mBiometricStateCallback); }); } @@ -777,4 +780,14 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi } return null; } + + @Override + public void scheduleWatchdog(int sensorId) { + Slog.d(getTag(), "Starting watchdog for fingerprint"); + final BiometricScheduler biometricScheduler = mSensors.get(sensorId).getScheduler(); + if (biometricScheduler == null) { + return; + } + biometricScheduler.startWatchdog(); + } } diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java index 0770062cd4d3..6a010424db13 100644 --- a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java +++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java @@ -29,6 +29,8 @@ import android.os.ServiceManager; import android.util.IndentingPrintWriter; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl; import com.android.server.utils.Slogf; import java.io.FileDescriptor; @@ -47,7 +49,7 @@ final class IRadioServiceAidlImpl extends IRadioService.Stub { private static final List<String> SERVICE_NAMES = Arrays.asList( IBroadcastRadio.DESCRIPTOR + "/amfm", IBroadcastRadio.DESCRIPTOR + "/dab"); - private final com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl mHalAidl; + private final BroadcastRadioServiceImpl mHalAidl; private final BroadcastRadioService mService; /** @@ -65,10 +67,15 @@ final class IRadioServiceAidlImpl extends IRadioService.Stub { } IRadioServiceAidlImpl(BroadcastRadioService service, ArrayList<String> serviceList) { + this(service, new BroadcastRadioServiceImpl(serviceList)); Slogf.i(TAG, "Initialize BroadcastRadioServiceAidl(%s)", service); - mService = Objects.requireNonNull(service); - mHalAidl = - new com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl(serviceList); + } + + @VisibleForTesting + IRadioServiceAidlImpl(BroadcastRadioService service, BroadcastRadioServiceImpl halAidl) { + mService = Objects.requireNonNull(service, "Broadcast radio service cannot be null"); + mHalAidl = Objects.requireNonNull(halAidl, + "Broadcast radio service implementation for AIDL HAL cannot be null"); } @Override @@ -96,8 +103,8 @@ final class IRadioServiceAidlImpl extends IRadioService.Stub { if (isDebugEnabled()) { Slogf.d(TAG, "Adding announcement listener for %s", Arrays.toString(enabledTypes)); } - Objects.requireNonNull(enabledTypes); - Objects.requireNonNull(listener); + Objects.requireNonNull(enabledTypes, "Enabled announcement types cannot be null"); + Objects.requireNonNull(listener, "Announcement listener cannot be null"); mService.enforcePolicyAccess(); return mHalAidl.addAnnouncementListener(enabledTypes, listener); diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java index 28b6d02581be..a8e4034e3f86 100644 --- a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java +++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java @@ -27,6 +27,7 @@ import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.broadcastradio.hal2.AnnouncementAggregator; import java.io.FileDescriptor; @@ -53,7 +54,7 @@ final class IRadioServiceHidlImpl extends IRadioService.Stub { private final List<RadioManager.ModuleProperties> mV1Modules; IRadioServiceHidlImpl(BroadcastRadioService service) { - mService = Objects.requireNonNull(service); + mService = Objects.requireNonNull(service, "broadcast radio service cannot be null"); mHal1 = new com.android.server.broadcastradio.hal1.BroadcastRadioService(mLock); mV1Modules = mHal1.loadModules(); OptionalInt max = mV1Modules.stream().mapToInt(RadioManager.ModuleProperties::getId).max(); @@ -61,6 +62,18 @@ final class IRadioServiceHidlImpl extends IRadioService.Stub { max.isPresent() ? max.getAsInt() + 1 : 0, mLock); } + @VisibleForTesting + IRadioServiceHidlImpl(BroadcastRadioService service, + com.android.server.broadcastradio.hal1.BroadcastRadioService hal1, + com.android.server.broadcastradio.hal2.BroadcastRadioService hal2) { + mService = Objects.requireNonNull(service, "Broadcast radio service cannot be null"); + mHal1 = Objects.requireNonNull(hal1, + "Broadcast radio service implementation for HIDL 1 HAL cannot be null"); + mV1Modules = mHal1.loadModules(); + mHal2 = Objects.requireNonNull(hal2, + "Broadcast radio service implementation for HIDL 2 HAL cannot be null"); + } + @Override public List<RadioManager.ModuleProperties> listModules() { mService.enforcePolicyAccess(); @@ -95,8 +108,8 @@ final class IRadioServiceHidlImpl extends IRadioService.Stub { if (isDebugEnabled()) { Slog.d(TAG, "Adding announcement listener for " + Arrays.toString(enabledTypes)); } - Objects.requireNonNull(enabledTypes); - Objects.requireNonNull(listener); + Objects.requireNonNull(enabledTypes, "Enabled announcement types cannot be null"); + Objects.requireNonNull(listener, "Announcement listener cannot be null"); mService.enforcePolicyAccess(); synchronized (mLock) { diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 587db41c0df8..5eb15e09f09e 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -40,6 +40,7 @@ import static android.os.Process.ROOT_UID; import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.AppOpsManager; import android.app.compat.CompatChanges; @@ -101,6 +102,7 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; +import android.provider.DeviceConfig; import android.provider.Settings; import android.sysprop.DisplayProperties; import android.text.TextUtils; @@ -202,8 +204,6 @@ public final class DisplayManagerService extends SystemService { private static final String FORCE_WIFI_DISPLAY_ENABLE = "persist.debug.wfd.enable"; private static final String PROP_DEFAULT_DISPLAY_TOP_INSET = "persist.sys.displayinset.top"; - private static final String PROP_USE_NEW_DISPLAY_POWER_CONTROLLER = - "persist.sys.use_new_display_power_controller"; private static final long WAIT_FOR_DEFAULT_DISPLAY_TIMEOUT = 10000; // This value needs to be in sync with the threshold // in RefreshRateConfigs::getFrameRateDivisor. @@ -1356,11 +1356,19 @@ public final class DisplayManagerService extends SystemService { final long token = Binder.clearCallingIdentity(); try { synchronized (mSyncRoot) { - final int displayId = createVirtualDisplayLocked(callback, projection, callingUid, - packageName, surface, flags, virtualDisplayConfig); + final int displayId = + createVirtualDisplayLocked( + callback, + projection, + callingUid, + packageName, + virtualDevice, + surface, + flags, + virtualDisplayConfig); if (displayId != Display.INVALID_DISPLAY && virtualDevice != null && dwpc != null) { - mDisplayWindowPolicyControllers.put(displayId, - Pair.create(virtualDevice, dwpc)); + mDisplayWindowPolicyControllers.put( + displayId, Pair.create(virtualDevice, dwpc)); } return displayId; } @@ -1369,12 +1377,20 @@ public final class DisplayManagerService extends SystemService { } } - private int createVirtualDisplayLocked(IVirtualDisplayCallback callback, - IMediaProjection projection, int callingUid, String packageName, Surface surface, - int flags, VirtualDisplayConfig virtualDisplayConfig) { + private int createVirtualDisplayLocked( + IVirtualDisplayCallback callback, + IMediaProjection projection, + int callingUid, + String packageName, + IVirtualDevice virtualDevice, + Surface surface, + int flags, + VirtualDisplayConfig virtualDisplayConfig) { if (mVirtualDisplayAdapter == null) { - Slog.w(TAG, "Rejecting request to create private virtual display " - + "because the virtual display adapter is not available."); + Slog.w( + TAG, + "Rejecting request to create private virtual display " + + "because the virtual display adapter is not available."); return -1; } @@ -1385,6 +1401,19 @@ public final class DisplayManagerService extends SystemService { return -1; } + // If the display is to be added to a device display group, we need to make the + // LogicalDisplayMapper aware of the link between the new display and its associated virtual + // device before triggering DISPLAY_DEVICE_EVENT_ADDED. + if (virtualDevice != null && (flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0) { + try { + final int virtualDeviceId = virtualDevice.getDeviceId(); + mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice( + device, virtualDeviceId); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + // DisplayDevice events are handled manually for Virtual Displays. // TODO: multi-display Fix this so that generic add/remove events are not handled in a // different code path for virtual displays. Currently this happens so that we can @@ -1393,8 +1422,7 @@ public final class DisplayManagerService extends SystemService { // called on the DisplayThread (which we don't want to wait for?). // One option would be to actually wait here on the binder thread // to be notified when the virtual display is created (or failed). - mDisplayDeviceRepo.onDisplayDeviceEvent(device, - DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED); + mDisplayDeviceRepo.onDisplayDeviceEvent(device, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED); final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device); if (display != null) { @@ -2575,6 +2603,7 @@ public final class DisplayManagerService extends SystemService { mLogicalDisplayMapper.forEachLocked(this::addDisplayPowerControllerLocked); } + @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) private void addDisplayPowerControllerLocked(LogicalDisplay display) { if (mPowerHandler == null) { // initPowerManagement has not yet been called. @@ -2588,7 +2617,8 @@ public final class DisplayManagerService extends SystemService { display, mSyncRoot); final DisplayPowerControllerInterface displayPowerController; - if (SystemProperties.getInt(PROP_USE_NEW_DISPLAY_POWER_CONTROLLER, 0) == 1) { + if (DeviceConfig.getBoolean("display_manager", + "use_newly_structured_display_power_controller", false)) { displayPowerController = new DisplayPowerController2( mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler, mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting, diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 70c9e23c6af8..cb97e2832854 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -28,6 +28,7 @@ import android.os.PowerManager; import android.os.SystemClock; import android.os.SystemProperties; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.Slog; @@ -123,6 +124,12 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { /** Map of all display groups indexed by display group id. */ private final SparseArray<DisplayGroup> mDisplayGroups = new SparseArray<>(); + /** + * Map of display groups which are linked to virtual devices (all displays in the group are + * linked to that device). Keyed by virtual device unique id. + */ + private final SparseIntArray mDeviceDisplayGroupIds = new SparseIntArray(); + private final DisplayDeviceRepository mDisplayDeviceRepo; private final DeviceStateToLayoutMap mDeviceStateToLayoutMap; private final Listener mListener; @@ -157,6 +164,12 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { */ private final SparseIntArray mDisplayGroupsToUpdate = new SparseIntArray(); + /** + * ArrayMap of display device unique ID to virtual device ID. Used in {@link + * #updateLogicalDisplaysLocked} to establish which Virtual Devices own which Virtual Displays. + */ + private final ArrayMap<String, Integer> mVirtualDeviceDisplayMapping = new ArrayMap<>(); + private int mNextNonDefaultGroupId = Display.DEFAULT_DISPLAY_GROUP + 1; private Layout mCurrentLayout = null; private int mDeviceState = DeviceStateManager.INVALID_DEVICE_STATE; @@ -362,6 +375,19 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { mDeviceStateToLayoutMap.dumpLocked(ipw); } + /** + * Creates an association between a displayDevice and a virtual device. Any displays associated + * with this virtual device will be grouped together in a single {@link DisplayGroup} unless + * created with {@link Display.FLAG_OWN_DISPLAY_GROUP}. + * + * @param displayDevice the displayDevice to be linked + * @param virtualDeviceUniqueId the unique ID of the virtual device. + */ + void associateDisplayDeviceWithVirtualDevice( + DisplayDevice displayDevice, int virtualDeviceUniqueId) { + mVirtualDeviceDisplayMapping.put(displayDevice.getUniqueId(), virtualDeviceUniqueId); + } + void setDeviceStateLocked(int state, boolean isOverrideActive) { Slog.i(TAG, "Requesting Transition to state: " + state + ", from state=" + mDeviceState + ", interactive=" + mInteractive); @@ -556,6 +582,9 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { } DisplayDeviceInfo deviceInfo = device.getDisplayDeviceInfoLocked(); + // Remove any virtual device mapping which exists for the display. + mVirtualDeviceDisplayMapping.remove(device.getUniqueId()); + if (layoutDisplay.getAddress().equals(deviceInfo.address)) { layout.removeDisplayLocked(DEFAULT_DISPLAY); @@ -749,24 +778,44 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { // We wait until we sent the EVENT_REMOVED event before actually removing the // group. mDisplayGroups.delete(id); + // Remove possible reference to the removed group. + int deviceIndex = mDeviceDisplayGroupIds.indexOfValue(id); + if (deviceIndex >= 0) { + mDeviceDisplayGroupIds.removeAt(deviceIndex); + } } } } private void assignDisplayGroupLocked(LogicalDisplay display) { final int displayId = display.getDisplayIdLocked(); + final String primaryDisplayUniqueId = display.getPrimaryDisplayDeviceLocked().getUniqueId(); + final Integer linkedDeviceUniqueId = + mVirtualDeviceDisplayMapping.get(primaryDisplayUniqueId); // Get current display group data int groupId = getDisplayGroupIdFromDisplayIdLocked(displayId); + Integer deviceDisplayGroupId = null; + if (linkedDeviceUniqueId != null + && mDeviceDisplayGroupIds.indexOfKey(linkedDeviceUniqueId) > 0) { + deviceDisplayGroupId = mDeviceDisplayGroupIds.get(linkedDeviceUniqueId); + } final DisplayGroup oldGroup = getDisplayGroupLocked(groupId); // Get the new display group if a change is needed final DisplayInfo info = display.getDisplayInfoLocked(); final boolean needsOwnDisplayGroup = (info.flags & Display.FLAG_OWN_DISPLAY_GROUP) != 0; final boolean hasOwnDisplayGroup = groupId != Display.DEFAULT_DISPLAY_GROUP; + final boolean needsDeviceDisplayGroup = + !needsOwnDisplayGroup && linkedDeviceUniqueId != null; + final boolean hasDeviceDisplayGroup = + deviceDisplayGroupId != null && groupId == deviceDisplayGroupId; if (groupId == Display.INVALID_DISPLAY_GROUP - || hasOwnDisplayGroup != needsOwnDisplayGroup) { - groupId = assignDisplayGroupIdLocked(needsOwnDisplayGroup); + || hasOwnDisplayGroup != needsOwnDisplayGroup + || hasDeviceDisplayGroup != needsDeviceDisplayGroup) { + groupId = + assignDisplayGroupIdLocked( + needsOwnDisplayGroup, needsDeviceDisplayGroup, linkedDeviceUniqueId); } // Create a new group if needed @@ -931,7 +980,17 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { display.setPhase(phase); } - private int assignDisplayGroupIdLocked(boolean isOwnDisplayGroup) { + private int assignDisplayGroupIdLocked( + boolean isOwnDisplayGroup, boolean isDeviceDisplayGroup, Integer linkedDeviceUniqueId) { + if (isDeviceDisplayGroup && linkedDeviceUniqueId != null) { + int deviceDisplayGroupId = mDeviceDisplayGroupIds.get(linkedDeviceUniqueId); + // A value of 0 indicates that no device display group was found. + if (deviceDisplayGroupId == 0) { + deviceDisplayGroupId = mNextNonDefaultGroupId++; + mDeviceDisplayGroupIds.put(linkedDeviceUniqueId, deviceDisplayGroupId); + } + return deviceDisplayGroupId; + } return isOwnDisplayGroup ? mNextNonDefaultGroupId++ : Display.DEFAULT_DISPLAY_GROUP; } diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java index b8af1bfcc254..819b719dd22e 100644 --- a/services/core/java/com/android/server/dreams/DreamController.java +++ b/services/core/java/com/android/server/dreams/DreamController.java @@ -16,6 +16,9 @@ package com.android.server.dreams; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; + +import android.app.ActivityTaskManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -34,8 +37,6 @@ import android.os.UserHandle; import android.service.dreams.DreamService; import android.service.dreams.IDreamService; import android.util.Slog; -import android.view.IWindowManager; -import android.view.WindowManagerGlobal; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -60,7 +61,7 @@ final class DreamController { private final Context mContext; private final Handler mHandler; private final Listener mListener; - private final IWindowManager mIWindowManager; + private final ActivityTaskManager mActivityTaskManager; private long mDreamStartTime; private String mSavedStopReason; @@ -93,7 +94,7 @@ final class DreamController { mContext = context; mHandler = handler; mListener = listener; - mIWindowManager = WindowManagerGlobal.getWindowManagerService(); + mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class); mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); mCloseNotificationShadeIntent.putExtra("reason", "dream"); } @@ -229,6 +230,8 @@ final class DreamController { } oldDream.releaseWakeLockIfNeeded(); + mActivityTaskManager.removeRootTasksWithActivityTypes(new int[] {ACTIVITY_TYPE_DREAM}); + mHandler.post(() -> mListener.onDreamStopped(oldDream.mToken)); } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index 324eefc809e8..06ee7c4d6c28 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -44,6 +44,8 @@ import java.io.PrintWriter; import java.util.Arrays; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; /** * A thread-safe component of {@link InputManagerService} responsible for managing the battery state @@ -98,8 +100,12 @@ final class BatteryController { } public void systemRunning() { - Objects.requireNonNull(mContext.getSystemService(InputManager.class)) - .registerInputDeviceListener(mInputDeviceListener, mHandler); + final InputManager inputManager = + Objects.requireNonNull(mContext.getSystemService(InputManager.class)); + inputManager.registerInputDeviceListener(mInputDeviceListener, mHandler); + for (int deviceId : inputManager.getInputDeviceIds()) { + mInputDeviceListener.onInputDeviceAdded(deviceId); + } } /** @@ -165,19 +171,20 @@ final class BatteryController { } } - @GuardedBy("mLock") - private void notifyAllListenersForDeviceLocked(State state) { - if (DEBUG) Slog.d(TAG, "Notifying all listeners of battery state: " + state); - mListenerRecords.forEach((pid, listenerRecord) -> { - if (listenerRecord.mMonitoredDevices.contains(state.deviceId)) { - notifyBatteryListener(listenerRecord, state); - } - }); + private void notifyAllListenersForDevice(State state) { + synchronized (mLock) { + if (DEBUG) Slog.d(TAG, "Notifying all listeners of battery state: " + state); + mListenerRecords.forEach((pid, listenerRecord) -> { + if (listenerRecord.mMonitoredDevices.contains(state.deviceId)) { + notifyBatteryListener(listenerRecord, state); + } + }); + } } @GuardedBy("mLock") private void updatePollingLocked(boolean delayStart) { - if (mDeviceMonitors.isEmpty() || !mIsInteractive) { + if (!mIsInteractive || !anyOf(mDeviceMonitors, DeviceMonitor::requiresPolling)) { // Stop polling. mIsPolling = false; mHandler.removeCallbacks(this::handlePollEvent); @@ -192,6 +199,13 @@ final class BatteryController { mHandler.postDelayed(this::handlePollEvent, delayStart ? POLLING_PERIOD_MILLIS : 0); } + private String getInputDeviceName(int deviceId) { + final InputDevice device = + Objects.requireNonNull(mContext.getSystemService(InputManager.class)) + .getInputDevice(deviceId); + return device != null ? device.getName() : "<none>"; + } + private boolean hasBattery(int deviceId) { final InputDevice device = Objects.requireNonNull(mContext.getSystemService(InputManager.class)) @@ -199,6 +213,13 @@ final class BatteryController { return device != null && device.hasBattery(); } + private boolean isUsiDevice(int deviceId) { + final InputDevice device = + Objects.requireNonNull(mContext.getSystemService(InputManager.class)) + .getInputDevice(deviceId); + return device != null && device.supportsUsi(); + } + @GuardedBy("mLock") private DeviceMonitor getDeviceMonitorOrThrowLocked(int deviceId) { return Objects.requireNonNull(mDeviceMonitors.get(deviceId), @@ -252,8 +273,10 @@ final class BatteryController { if (!hasRegisteredListenerForDeviceLocked(deviceId)) { // There are no more listeners monitoring this device. final DeviceMonitor monitor = getDeviceMonitorOrThrowLocked(deviceId); - monitor.stopMonitoring(); - mDeviceMonitors.remove(deviceId); + if (!monitor.isPersistent()) { + monitor.onMonitorDestroy(); + mDeviceMonitors.remove(deviceId); + } } if (listenerRecord.mMonitoredDevices.isEmpty()) { @@ -298,9 +321,7 @@ final class BatteryController { if (monitor == null) { return; } - if (monitor.updateBatteryState(eventTime)) { - notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting()); - } + monitor.onUEvent(eventTime); } } @@ -310,14 +331,7 @@ final class BatteryController { return; } final long eventTime = SystemClock.uptimeMillis(); - mDeviceMonitors.forEach((deviceId, monitor) -> { - // Re-acquire lock in the lambda to silence error-prone build warnings. - synchronized (mLock) { - if (monitor.updateBatteryState(eventTime)) { - notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting()); - } - } - }); + mDeviceMonitors.forEach((deviceId, monitor) -> monitor.onPoll(eventTime)); mHandler.postDelayed(this::handlePollEvent, POLLING_PERIOD_MILLIS); } } @@ -329,15 +343,11 @@ final class BatteryController { final DeviceMonitor monitor = mDeviceMonitors.get(deviceId); if (monitor == null) { // The input device's battery is not being monitored by any listener. - return queryBatteryStateFromNative(deviceId, updateTime); + return queryBatteryStateFromNative(deviceId, updateTime, hasBattery(deviceId)); } // Force the battery state to update, and notify listeners if necessary. - final boolean stateChanged = monitor.updateBatteryState(updateTime); - final State state = monitor.getBatteryStateForReporting(); - if (stateChanged) { - notifyAllListenersForDeviceLocked(state); - } - return state; + monitor.onPoll(updateTime); + return monitor.getBatteryStateForReporting(); } } @@ -379,7 +389,14 @@ final class BatteryController { private final InputManager.InputDeviceListener mInputDeviceListener = new InputManager.InputDeviceListener() { @Override - public void onInputDeviceAdded(int deviceId) {} + public void onInputDeviceAdded(int deviceId) { + synchronized (mLock) { + if (isUsiDevice(deviceId) && !mDeviceMonitors.containsKey(deviceId)) { + // Start monitoring USI device immediately. + mDeviceMonitors.put(deviceId, new UsiDeviceMonitor(deviceId)); + } + } + } @Override public void onInputDeviceRemoved(int deviceId) {} @@ -392,9 +409,7 @@ final class BatteryController { return; } final long eventTime = SystemClock.uptimeMillis(); - if (monitor.updateBatteryState(eventTime)) { - notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting()); - } + monitor.onConfiguration(eventTime); } } }; @@ -422,8 +437,7 @@ final class BatteryController { } // Queries the battery state of an input device from native code. - private State queryBatteryStateFromNative(int deviceId, long updateTime) { - final boolean isPresent = hasBattery(deviceId); + private State queryBatteryStateFromNative(int deviceId, long updateTime, boolean isPresent) { return new State( deviceId, updateTime, @@ -434,8 +448,9 @@ final class BatteryController { // Holds the state of an InputDevice for which battery changes are currently being monitored. private class DeviceMonitor { - @NonNull - private State mState; + private final State mState; + // Represents whether the input device has a sysfs battery node. + protected boolean mHasBattery = false; @Nullable private UEventBatteryListener mUEventBatteryListener; @@ -445,26 +460,32 @@ final class BatteryController { // Load the initial battery state and start monitoring. final long eventTime = SystemClock.uptimeMillis(); - updateBatteryState(eventTime); + configureDeviceMonitor(eventTime); } - // Returns true if the battery state changed since the last time it was updated. - public boolean updateBatteryState(long updateTime) { - mState.updateTime = updateTime; - - final State updatedState = queryBatteryStateFromNative(mState.deviceId, updateTime); - if (mState.equals(updatedState)) { - return false; + private void processChangesAndNotify(long eventTime, Consumer<Long> changes) { + final State oldState = getBatteryStateForReporting(); + changes.accept(eventTime); + final State newState = getBatteryStateForReporting(); + if (!oldState.equals(newState)) { + notifyAllListenersForDevice(newState); } - if (mState.isPresent != updatedState.isPresent) { - if (updatedState.isPresent) { + } + + public void onConfiguration(long eventTime) { + processChangesAndNotify(eventTime, this::configureDeviceMonitor); + } + + private void configureDeviceMonitor(long eventTime) { + if (mHasBattery != hasBattery(mState.deviceId)) { + mHasBattery = !mHasBattery; + if (mHasBattery) { startMonitoring(); } else { stopMonitoring(); } + updateBatteryStateFromNative(eventTime); } - mState = updatedState; - return true; } private void startMonitoring() { @@ -483,19 +504,44 @@ final class BatteryController { mUEventBatteryListener, "DEVPATH=" + formatDevPath(batteryPath)); } - private String formatDevPath(String path) { + private String formatDevPath(@NonNull String path) { // Remove the "/sys" prefix if it has one. return path.startsWith("/sys") ? path.substring(4) : path; } - // This must be called when the device is no longer being monitored. - public void stopMonitoring() { + private void stopMonitoring() { if (mUEventBatteryListener != null) { mUEventManager.removeListener(mUEventBatteryListener); mUEventBatteryListener = null; } } + // This must be called when the device is no longer being monitored. + public void onMonitorDestroy() { + stopMonitoring(); + } + + private void updateBatteryStateFromNative(long eventTime) { + mState.updateIfChanged( + queryBatteryStateFromNative(mState.deviceId, eventTime, mHasBattery)); + } + + public void onPoll(long eventTime) { + processChangesAndNotify(eventTime, this::updateBatteryStateFromNative); + } + + public void onUEvent(long eventTime) { + processChangesAndNotify(eventTime, this::updateBatteryStateFromNative); + } + + public boolean requiresPolling() { + return true; + } + + public boolean isPersistent() { + return false; + } + // Returns the current battery state that can be used to notify listeners BatteryController. public State getBatteryStateForReporting() { return new State(mState); @@ -503,8 +549,31 @@ final class BatteryController { @Override public String toString() { - return "state=" + mState - + ", uEventListener=" + (mUEventBatteryListener != null ? "added" : "none"); + return "DeviceId=" + mState.deviceId + + ", Name='" + getInputDeviceName(mState.deviceId) + "'" + + ", NativeBattery=" + mState + + ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none"); + } + } + + // Battery monitoring logic that is specific to stylus devices that support the + // Universal Stylus Initiative (USI) protocol. + private class UsiDeviceMonitor extends DeviceMonitor { + + UsiDeviceMonitor(int deviceId) { + super(deviceId); + } + + @Override + public boolean requiresPolling() { + // Do not poll the battery state for USI devices. + return false; + } + + @Override + public boolean isPersistent() { + // Do not remove the battery monitor for USI devices. + return true; } } @@ -548,18 +617,33 @@ final class BatteryController { private static class State extends IInputDeviceBatteryState { State(int deviceId) { - initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN, - Float.NaN /*capacity*/); + reset(deviceId); } State(IInputDeviceBatteryState s) { - initialize(s.deviceId, s.updateTime, s.isPresent, s.status, s.capacity); + copyFrom(s); } State(int deviceId, long updateTime, boolean isPresent, int status, float capacity) { initialize(deviceId, updateTime, isPresent, status, capacity); } + // Updates this from other if there is a difference between them, ignoring the updateTime. + public void updateIfChanged(IInputDeviceBatteryState other) { + if (!equalsIgnoringUpdateTime(other)) { + copyFrom(other); + } + } + + private void reset(int deviceId) { + initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN, + Float.NaN /*capacity*/); + } + + private void copyFrom(IInputDeviceBatteryState s) { + initialize(s.deviceId, s.updateTime, s.isPresent, s.status, s.capacity); + } + private void initialize(int deviceId, long updateTime, boolean isPresent, int status, float capacity) { this.deviceId = deviceId; @@ -569,11 +653,34 @@ final class BatteryController { this.capacity = capacity; } + private boolean equalsIgnoringUpdateTime(IInputDeviceBatteryState other) { + long updateTime = this.updateTime; + this.updateTime = other.updateTime; + boolean eq = this.equals(other); + this.updateTime = updateTime; + return eq; + } + @Override public String toString() { - return "BatteryState{deviceId=" + deviceId + ", updateTime=" + updateTime - + ", isPresent=" + isPresent + ", status=" + status + ", capacity=" + capacity - + " }"; + if (!isPresent) { + return "State{<not present>}"; + } + return "State{time=" + updateTime + + ", isPresent=" + isPresent + + ", status=" + status + + ", capacity=" + capacity + + "}"; + } + } + + // Check if any value in an ArrayMap matches the predicate in an optimized way. + private static <K, V> boolean anyOf(ArrayMap<K, V> arrayMap, Predicate<V> test) { + for (int i = 0; i < arrayMap.size(); i++) { + if (test.test(arrayMap.valueAt(i))) { + return true; + } } + return false; } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 76331fd6089c..76495b17c984 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -58,7 +58,6 @@ import android.Manifest; import android.accessibilityservice.AccessibilityService; import android.annotation.AnyThread; import android.annotation.BinderThread; -import android.annotation.ColorInt; import android.annotation.DrawableRes; import android.annotation.DurationMillisLong; import android.annotation.EnforcePermission; @@ -69,9 +68,6 @@ import android.annotation.UiThread; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentProvider; @@ -94,7 +90,6 @@ import android.inputmethodservice.InputMethodService; import android.media.AudioManagerInternal; import android.net.Uri; import android.os.Binder; -import android.os.Bundle; import android.os.Debug; import android.os.Handler; import android.os.IBinder; @@ -170,8 +165,6 @@ import com.android.internal.inputmethod.SoftInputShowHideReason; import com.android.internal.inputmethod.StartInputFlags; import com.android.internal.inputmethod.StartInputReason; import com.android.internal.inputmethod.UnbindReason; -import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; -import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.os.TransferPipe; import com.android.internal.util.ArrayUtils; import com.android.internal.util.ConcurrentUtils; @@ -255,13 +248,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private static final String HANDLER_THREAD_NAME = "android.imms"; /** - * A protected broadcast intent action for internal use for {@link PendingIntent} in - * the notification. - */ - private static final String ACTION_SHOW_INPUT_METHOD_PICKER = - "com.android.server.inputmethod.InputMethodManagerService.SHOW_INPUT_METHOD_PICKER"; - - /** * When set, {@link #startInputUncheckedLocked} will return * {@link InputBindResult#NO_EDITOR} instead of starting an IME connection * unless {@link StartInputFlags#IS_TEXT_EDITOR} is set. This behavior overrides @@ -334,13 +320,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private int mDisplayIdToShowIme = INVALID_DISPLAY; - // Ongoing notification - private NotificationManager mNotificationManager; @Nullable private StatusBarManagerInternal mStatusBarManagerInternal; - private final Notification.Builder mImeSwitcherNotification; - private final PendingIntent mImeSwitchPendingIntent; private boolean mShowOngoingImeSwitcherForPhones; - private boolean mNotificationShown; @GuardedBy("ImfLock.class") private final HandwritingModeController mHwController; @GuardedBy("ImfLock.class") @@ -1253,17 +1234,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return; } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { onActionLocaleChanged(); - } else if (ACTION_SHOW_INPUT_METHOD_PICKER.equals(action)) { - // ACTION_SHOW_INPUT_METHOD_PICKER action is a protected-broadcast and it is - // guaranteed to be send only from the system, so that there is no need for extra - // security check such as - // {@link #canShowInputMethodPickerLocked(IInputMethodClient)}. - mHandler.obtainMessage( - MSG_SHOW_IM_SUBTYPE_PICKER, - // TODO(b/120076400): Design and implement IME switcher for heterogeneous - // navbar configuration. - InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES, - DEFAULT_DISPLAY).sendToTarget(); } else { Slog.w(TAG, "Unexpected intent " + intent); } @@ -1720,27 +1690,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mSlotIme = mContext.getString(com.android.internal.R.string.status_bar_ime); - Bundle extras = new Bundle(); - extras.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, true); - @ColorInt final int accentColor = mContext.getColor( - com.android.internal.R.color.system_notification_accent_color); - mImeSwitcherNotification = - new Notification.Builder(mContext, SystemNotificationChannels.VIRTUAL_KEYBOARD) - .setSmallIcon(com.android.internal.R.drawable.ic_notification_ime_default) - .setWhen(0) - .setOngoing(true) - .addExtras(extras) - .setCategory(Notification.CATEGORY_SYSTEM) - .setColor(accentColor); - - Intent intent = new Intent(ACTION_SHOW_INPUT_METHOD_PICKER) - .setPackage(mContext.getPackageName()); - mImeSwitchPendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, - PendingIntent.FLAG_IMMUTABLE); - mShowOngoingImeSwitcherForPhones = false; - mNotificationShown = false; final int userId = mActivityManagerInternal.getCurrentUserId(); mLastSwitchUserId = userId; @@ -1939,7 +1890,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final int currentUserId = mSettings.getCurrentUserId(); mSettings.switchCurrentUser(currentUserId, !mUserManagerInternal.isUserUnlockingOrUnlocked(currentUserId)); - mNotificationManager = mContext.getSystemService(NotificationManager.class); mStatusBarManagerInternal = LocalServices.getService(StatusBarManagerInternal.class); hideStatusBarIconLocked(); @@ -1977,7 +1927,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub broadcastFilterForSystemUser.addAction(Intent.ACTION_USER_ADDED); broadcastFilterForSystemUser.addAction(Intent.ACTION_USER_REMOVED); broadcastFilterForSystemUser.addAction(Intent.ACTION_LOCALE_CHANGED); - broadcastFilterForSystemUser.addAction(ACTION_SHOW_INPUT_METHOD_PICKER); mContext.registerReceiver(new ImmsBroadcastReceiverForSystemUser(), broadcastFilterForSystemUser); @@ -3159,41 +3108,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mStatusBarManagerInternal.setImeWindowStatus(mCurTokenDisplayId, getCurTokenLocked(), vis, backDisposition, needsToShowImeSwitcher); } - final InputMethodInfo imi = mMethodMap.get(getSelectedMethodIdLocked()); - if (imi != null && needsToShowImeSwitcher) { - // Used to load label - final CharSequence title = mRes.getText( - com.android.internal.R.string.select_input_method); - final int currentUserId = mSettings.getCurrentUserId(); - final Context userAwareContext = mContext.getUserId() == currentUserId - ? mContext - : mContext.createContextAsUser(UserHandle.of(currentUserId), 0 /* flags */); - final CharSequence summary = InputMethodUtils.getImeAndSubtypeDisplayName( - userAwareContext, imi, mCurrentSubtype); - mImeSwitcherNotification.setContentTitle(title) - .setContentText(summary) - .setContentIntent(mImeSwitchPendingIntent); - // TODO(b/120076400): Figure out what is the best behavior - if ((mNotificationManager != null) - && !mWindowManagerInternal.hasNavigationBar(DEFAULT_DISPLAY)) { - if (DEBUG) { - Slog.d(TAG, "--- show notification: label = " + summary); - } - mNotificationManager.notifyAsUser(null, - SystemMessage.NOTE_SELECT_INPUT_METHOD, - mImeSwitcherNotification.build(), UserHandle.ALL); - mNotificationShown = true; - } - } else { - if (mNotificationShown && mNotificationManager != null) { - if (DEBUG) { - Slog.d(TAG, "--- hide notification"); - } - mNotificationManager.cancelAsUser(null, - SystemMessage.NOTE_SELECT_INPUT_METHOD, UserHandle.ALL); - mNotificationShown = false; - } - } } finally { Binder.restoreCallingIdentity(ident); } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java index c7ff8caf176b..ebf9237d61ea 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java @@ -179,16 +179,6 @@ final class InputMethodUtils { } } - static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi, - InputMethodSubtype subtype) { - final CharSequence imiLabel = imi.loadLabel(context.getPackageManager()); - return subtype != null - ? TextUtils.concat(subtype.getDisplayName(context, - imi.getPackageName(), imi.getServiceInfo().applicationInfo), - (TextUtils.isEmpty(imiLabel) ? "" : " - " + imiLabel)) - : imiLabel; - } - /** * Returns true if a package name belongs to a UID. * diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 77fea09b5ecc..f459c0e5eeb4 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -61,6 +61,7 @@ import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_TELECOM; import static android.content.pm.PackageManager.FEATURE_TELEVISION; import static android.content.pm.PackageManager.MATCH_ALL; +import static android.content.pm.PackageManager.MATCH_ANY_USER; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -10700,10 +10701,18 @@ public class NotificationManagerService extends SystemService { private final ArraySet<ManagedServiceInfo> mLightTrimListeners = new ArraySet<>(); ArrayMap<Pair<ComponentName, Integer>, NotificationListenerFilter> mRequestedNotificationListeners = new ArrayMap<>(); + private final boolean mIsHeadlessSystemUserMode; public NotificationListeners(Context context, Object lock, UserProfiles userProfiles, IPackageManager pm) { + this(context, lock, userProfiles, pm, UserManager.isHeadlessSystemUserMode()); + } + + @VisibleForTesting + public NotificationListeners(Context context, Object lock, UserProfiles userProfiles, + IPackageManager pm, boolean isHeadlessSystemUserMode) { super(context, lock, userProfiles, pm); + this.mIsHeadlessSystemUserMode = isHeadlessSystemUserMode; } @Override @@ -10728,10 +10737,16 @@ public class NotificationManagerService extends SystemService { if (TextUtils.isEmpty(listeners[i])) { continue; } + int packageQueryFlags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE; + // In the headless system user mode, packages might not be installed for the + // system user. Match packages for any user since apps can be installed only for + // non-system users and would be considering uninstalled for the system user. + if (mIsHeadlessSystemUserMode) { + packageQueryFlags += MATCH_ANY_USER; + } ArraySet<ComponentName> approvedListeners = - this.queryPackageForServices(listeners[i], - MATCH_DIRECT_BOOT_AWARE - | MATCH_DIRECT_BOOT_UNAWARE, USER_SYSTEM); + this.queryPackageForServices(listeners[i], packageQueryFlags, + USER_SYSTEM); for (int k = 0; k < approvedListeners.size(); k++) { ComponentName cn = approvedListeners.valueAt(k); addDefaultComponentOrPackage(cn.flattenToString()); diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java index 8e672c3b32c5..17bb39c945bd 100644 --- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java +++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java @@ -166,6 +166,14 @@ final class OverlayManagerServiceImpl { CollectionUtils.addAll(updatedTargets, removeOverlaysForUser( (info) -> !userPackages.containsKey(info.packageName), newUserId)); + final ArraySet<String> overlaidByOthers = new ArraySet<>(); + for (AndroidPackage androidPackage : userPackages.values()) { + final String overlayTarget = androidPackage.getOverlayTarget(); + if (!TextUtils.isEmpty(overlayTarget)) { + overlaidByOthers.add(overlayTarget); + } + } + // Update the state of all installed packages containing overlays, and initialize new // overlays that are not currently in the settings. for (int i = 0, n = userPackages.size(); i < n; i++) { @@ -175,8 +183,10 @@ final class OverlayManagerServiceImpl { updatePackageOverlays(pkg, newUserId, 0 /* flags */)); // When a new user is switched to for the first time, package manager must be - // informed of the overlay paths for all packages installed in the user. - updatedTargets.add(new PackageAndUser(pkg.getPackageName(), newUserId)); + // informed of the overlay paths for all overlaid packages installed in the user. + if (overlaidByOthers.contains(pkg.getPackageName())) { + updatedTargets.add(new PackageAndUser(pkg.getPackageName(), newUserId)); + } } catch (OperationFailedException e) { Slog.e(TAG, "failed to initialize overlays of '" + pkg.getPackageName() + "' for user " + newUserId + "", e); diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java index a4e295b4f7df..bf00a33d7d20 100644 --- a/services/core/java/com/android/server/pm/Computer.java +++ b/services/core/java/com/android/server/pm/Computer.java @@ -203,6 +203,12 @@ public interface Computer extends PackageDataSnapshot { boolean filterSharedLibPackage(@Nullable PackageStateInternal ps, int uid, int userId, long flags); boolean isCallerSameApp(String packageName, int uid); + /** + * Returns true if the package name and the uid represent the same app. + * + * @param resolveIsolatedUid if true, resolves an isolated uid into the real uid. + */ + boolean isCallerSameApp(String packageName, int uid, boolean resolveIsolatedUid); boolean isComponentVisibleToInstantApp(@Nullable ComponentName component); boolean isComponentVisibleToInstantApp(@Nullable ComponentName component, @PackageManager.ComponentType int type); diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 5d479d52d6cc..86b8272dbe00 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -2209,11 +2209,19 @@ public class ComputerEngine implements Computer { } public final boolean isCallerSameApp(String packageName, int uid) { + return isCallerSameApp(packageName, uid, false /* resolveIsolatedUid */); + } + + @Override + public final boolean isCallerSameApp(String packageName, int uid, boolean resolveIsolatedUid) { if (Process.isSdkSandboxUid(uid)) { return (packageName != null && packageName.equals(mService.getSdkSandboxPackageName())); } AndroidPackage pkg = mPackages.get(packageName); + if (resolveIsolatedUid && Process.isIsolated(uid)) { + uid = getIsolatedOwner(uid); + } return pkg != null && UserHandle.getAppId(uid) == pkg.getUid(); } diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java index 3f04264714e4..c4f6836eba7b 100644 --- a/services/core/java/com/android/server/pm/DexOptHelper.java +++ b/services/core/java/com/android/server/pm/DexOptHelper.java @@ -18,6 +18,7 @@ package com.android.server.pm; import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; +import static com.android.server.LocalManagerRegistry.ManagerNotFoundException; import static com.android.server.pm.ApexManager.ActiveApexInfo; import static com.android.server.pm.InstructionSets.getAppDexInstructionSets; import static com.android.server.pm.PackageManagerService.DEBUG_DEXOPT; @@ -34,6 +35,7 @@ import static com.android.server.pm.PackageManagerServiceUtils.REMOVE_IF_NULL_PK import android.Manifest; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.app.ActivityManager; import android.app.AppGlobals; @@ -56,9 +58,16 @@ import android.util.Slog; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; +import com.android.server.LocalManagerRegistry; +import com.android.server.art.ArtManagerLocal; +import com.android.server.art.model.ArtFlags; +import com.android.server.art.model.OptimizeParams; +import com.android.server.art.model.OptimizeResult; +import com.android.server.pm.PackageDexOptimizer.DexOptResult; import com.android.server.pm.dex.DexManager; import com.android.server.pm.dex.DexoptOptions; import com.android.server.pm.pkg.AndroidPackage; +import com.android.server.pm.pkg.PackageState; import com.android.server.pm.pkg.PackageStateInternal; import dalvik.system.DexFile; @@ -72,11 +81,15 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; -final class DexOptHelper { +/** + * Helper class for dex optimization operations in PackageManagerService. + */ +public final class DexOptHelper { private static final long SEVEN_DAYS_IN_MILLISECONDS = 7 * 24 * 60 * 60 * 1000; private final PackageManagerService mPm; @@ -405,11 +418,12 @@ final class DexOptHelper { * {@link PackageDexOptimizer#DEX_OPT_CANCELLED} * {@link PackageDexOptimizer#DEX_OPT_FAILED} */ - @PackageDexOptimizer.DexOptResult + @DexOptResult /* package */ int performDexOptWithStatus(DexoptOptions options) { return performDexOptTraced(options); } + @DexOptResult private int performDexOptTraced(DexoptOptions options) { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt"); try { @@ -421,7 +435,13 @@ final class DexOptHelper { // Run dexopt on a given package. Returns true if dexopt did not fail, i.e. // if the package can now be considered up to date for the given filter. + @DexOptResult private int performDexOptInternal(DexoptOptions options) { + Optional<Integer> artSrvRes = performDexOptWithArtService(options); + if (artSrvRes.isPresent()) { + return artSrvRes.get(); + } + AndroidPackage p; PackageSetting pkgSetting; synchronized (mPm.mLock) { @@ -446,8 +466,74 @@ final class DexOptHelper { } } - private int performDexOptInternalWithDependenciesLI(AndroidPackage p, - @NonNull PackageStateInternal pkgSetting, DexoptOptions options) { + /** + * Performs dexopt on the given package using ART Service. + * + * @return a {@link DexOptResult}, or empty if the request isn't supported so that it is + * necessary to fall back to the legacy code paths. + */ + private Optional<Integer> performDexOptWithArtService(DexoptOptions options) { + ArtManagerLocal artManager = getArtManagerLocal(); + if (artManager == null) { + return Optional.empty(); + } + + try (PackageManagerLocal.FilteredSnapshot snapshot = + getPackageManagerLocal().withFilteredSnapshot()) { + PackageState ops = snapshot.getPackageState(options.getPackageName()); + if (ops == null) { + return Optional.of(PackageDexOptimizer.DEX_OPT_FAILED); + } + AndroidPackage oap = ops.getAndroidPackage(); + if (oap == null) { + return Optional.of(PackageDexOptimizer.DEX_OPT_FAILED); + } + if (oap.isApex()) { + return Optional.of(PackageDexOptimizer.DEX_OPT_SKIPPED); + } + + // TODO(b/245301593): Delete the conditional when ART Service supports + // FLAG_SHOULD_INCLUDE_DEPENDENCIES and we can just set it unconditionally. + /*@OptimizeFlags*/ int extraFlags = ops.getUsesLibraries().isEmpty() + ? 0 + : ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES; + + OptimizeParams params = options.convertToOptimizeParams(extraFlags); + if (params == null) { + return Optional.empty(); + } + + // TODO(b/251903639): Either remove controlDexOptBlocking, or don't ignore it here. + OptimizeResult result; + try { + result = artManager.optimizePackage(snapshot, options.getPackageName(), params); + } catch (UnsupportedOperationException e) { + reportArtManagerFallback(options.getPackageName(), e.toString()); + return Optional.empty(); + } + + // TODO(b/251903639): Move this to ArtManagerLocal.addOptimizePackageDoneCallback when + // it is implemented. + for (OptimizeResult.PackageOptimizeResult pkgRes : result.getPackageOptimizeResults()) { + PackageState ps = snapshot.getPackageState(pkgRes.getPackageName()); + AndroidPackage ap = ps != null ? ps.getAndroidPackage() : null; + if (ap != null) { + CompilerStats.PackageStats stats = mPm.getOrCreateCompilerPackageStats(ap); + for (OptimizeResult.DexContainerFileOptimizeResult dexRes : + pkgRes.getDexContainerFileOptimizeResults()) { + stats.setCompileTime( + dexRes.getDexContainerFile(), dexRes.getDex2oatWallTimeMillis()); + } + } + } + + return Optional.of(convertToDexOptResult(result)); + } + } + + @DexOptResult + private int performDexOptInternalWithDependenciesLI( + AndroidPackage p, @NonNull PackageStateInternal pkgSetting, DexoptOptions options) { // System server gets a special path. if (PLATFORM_PACKAGE_NAME.equals(p.getPackageName())) { return mPm.getDexManager().dexoptSystemServer(options); @@ -514,10 +600,20 @@ final class DexOptHelper { // Whoever is calling forceDexOpt wants a compiled package. // Don't use profiles since that may cause compilation to be skipped. - final int res = performDexOptInternalWithDependenciesLI(pkg, packageState, - new DexoptOptions(packageName, REASON_CMDLINE, - getDefaultCompilerFilter(), null /* splitName */, - DexoptOptions.DEXOPT_FORCE | DexoptOptions.DEXOPT_BOOT_COMPLETE)); + DexoptOptions options = new DexoptOptions(packageName, REASON_CMDLINE, + getDefaultCompilerFilter(), null /* splitName */, + DexoptOptions.DEXOPT_FORCE | DexoptOptions.DEXOPT_BOOT_COMPLETE); + + // performDexOptWithArtService ignores the snapshot and takes its own, so it can race with + // the package checks above, but at worst the effect is only a bit less friendly error + // below. + Optional<Integer> artSrvRes = performDexOptWithArtService(options); + int res; + if (artSrvRes.isPresent()) { + res = artSrvRes.get(); + } else { + res = performDexOptInternalWithDependenciesLI(pkg, packageState, options); + } Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); if (res != PackageDexOptimizer.DEX_OPT_PERFORMED) { @@ -800,4 +896,59 @@ final class DexOptHelper { } return false; } + + private @NonNull PackageManagerLocal getPackageManagerLocal() { + try { + return LocalManagerRegistry.getManagerOrThrow(PackageManagerLocal.class); + } catch (ManagerNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** + * Called whenever we need to fall back from ART Service to the legacy dexopt code. + */ + public static void reportArtManagerFallback(String packageName, String reason) { + // STOPSHIP(b/251903639): Minimize these calls to avoid platform getting shipped with code + // paths that will always bypass ART Service. + Slog.i(TAG, "Falling back to old PackageManager dexopt for " + packageName + ": " + reason); + } + + /** + * Returns {@link ArtManagerLocal} if one is found and should be used for package optimization. + */ + private @Nullable ArtManagerLocal getArtManagerLocal() { + if (!"true".equals(SystemProperties.get("dalvik.vm.useartservice", ""))) { + return null; + } + try { + return LocalManagerRegistry.getManagerOrThrow(ArtManagerLocal.class); + } catch (ManagerNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** + * Converts an ART Service {@link OptimizeResult} to {@link DexOptResult}. + * + * For interfacing {@link ArtManagerLocal} with legacy dex optimization code in PackageManager. + */ + @DexOptResult + private static int convertToDexOptResult(OptimizeResult result) { + /*@OptimizeStatus*/ int status = result.getFinalStatus(); + switch (status) { + case OptimizeResult.OPTIMIZE_SKIPPED: + return PackageDexOptimizer.DEX_OPT_SKIPPED; + case OptimizeResult.OPTIMIZE_FAILED: + return PackageDexOptimizer.DEX_OPT_FAILED; + case OptimizeResult.OPTIMIZE_PERFORMED: + return PackageDexOptimizer.DEX_OPT_PERFORMED; + case OptimizeResult.OPTIMIZE_CANCELLED: + return PackageDexOptimizer.DEX_OPT_CANCELLED; + default: + throw new IllegalArgumentException("OptimizeResult for " + + result.getPackageOptimizeResults().get(0).getPackageName() + + " has unsupported status " + status); + } + } } diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java index d25bca76245b..2a2410fd1767 100644 --- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java +++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java @@ -652,12 +652,6 @@ public class PackageDexOptimizer { @DexOptResult private int dexOptSecondaryDexPathLI(ApplicationInfo info, String path, PackageDexUsage.DexUseInfo dexUseInfo, DexoptOptions options) { - if (options.isDexoptOnlySharedDex() && !dexUseInfo.isUsedByOtherApps()) { - // We are asked to optimize only the dex files used by other apps and this is not - // on of them: skip it. - return DEX_OPT_SKIPPED; - } - String compilerFilter = getRealCompilerFilter(info, options.getCompilerFilter(), dexUseInfo.isUsedByOtherApps()); // Get the dexopt flags after getRealCompilerFilter to make sure we get the correct flags. diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 8fed153825db..6e54d0bbd656 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -5242,25 +5242,30 @@ public class PackageManagerService implements PackageSender, TestUtilityService Map<String, String> classLoaderContextMap, String loaderIsa) { int callingUid = Binder.getCallingUid(); - if (PackageManagerService.PLATFORM_PACKAGE_NAME.equals(loadingPackageName) - && callingUid != Process.SYSTEM_UID) { + + // TODO(b/254043366): System server should not report its own dex load because there's + // nothing ART can do with it. + + Computer snapshot = snapshot(); + + // System server should be able to report dex load on behalf of other apps. E.g., it + // could potentially resend the notifications in order to migrate the existing dex load + // info to ART Service. + if (!PackageManagerServiceUtils.isSystemOrRoot() + && !snapshot.isCallerSameApp( + loadingPackageName, callingUid, true /* resolveIsolatedUid */)) { Slog.w(PackageManagerService.TAG, - "Non System Server process reporting dex loads as system server. uid=" - + callingUid); - // Do not record dex loads from processes pretending to be system server. - // Only the system server should be assigned the package "android", so reject calls - // that don't satisfy the constraint. - // - // notifyDexLoad is a PM API callable from the app process. So in theory, apps could - // craft calls to this API and pretend to be system server. Doing so poses no - // particular danger for dex load reporting or later dexopt, however it is a - // sensible check to do in order to verify the expectations. + TextUtils.formatSimple( + "Invalid dex load report. loadingPackageName=%s, uid=%d", + loadingPackageName, callingUid)); return; } + // TODO(b/254043366): Call `ArtManagerLocal.notifyDexLoad`. + int userId = UserHandle.getCallingUserId(); - ApplicationInfo ai = snapshot().getApplicationInfo(loadingPackageName, /*flags*/ 0, - userId); + ApplicationInfo ai = + snapshot.getApplicationInfo(loadingPackageName, /*flags*/ 0, userId); if (ai == null) { Slog.w(PackageManagerService.TAG, "Loading a package that does not exist for the calling user. package=" + loadingPackageName + ", user=" + userId); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 60f247843bb7..21191919c8e0 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -2633,6 +2633,9 @@ public class UserManagerService extends IUserManager.Stub { /** @return a specific user restriction that's in effect currently. */ @Override public boolean hasUserRestriction(String restrictionKey, @UserIdInt int userId) { + if (!userExists(userId)) { + return false; + } checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "hasUserRestriction"); return mLocalService.hasUserRestriction(restrictionKey, userId); } @@ -5516,6 +5519,13 @@ public class UserManagerService extends IUserManager.Stub { private void removeUserState(final @UserIdInt int userId) { Slog.i(LOG_TAG, "Removing user state of user " + userId); + + // Cleanup lock settings. This must happen before destroyUserKey(), since the user's DE + // storage must still be accessible for the lock settings state to be properly cleaned up. + mLockPatternUtils.removeUser(userId); + + // Evict and destroy the user's CE and DE encryption keys. At this point, the user's CE and + // DE storage is made inaccessible, except to delete its contents. try { mContext.getSystemService(StorageManager.class).destroyUserKey(userId); } catch (IllegalStateException e) { @@ -5523,9 +5533,6 @@ public class UserManagerService extends IUserManager.Stub { Slog.i(LOG_TAG, "Destroying key for user " + userId + " failed, continuing anyway", e); } - // Cleanup lock settings - mLockPatternUtils.removeUser(userId); - // Cleanup package manager settings mPm.cleanUpUser(this, userId); diff --git a/services/core/java/com/android/server/pm/dex/DexoptOptions.java b/services/core/java/com/android/server/pm/dex/DexoptOptions.java index ea233161b4af..f5557c417f1b 100644 --- a/services/core/java/com/android/server/pm/dex/DexoptOptions.java +++ b/services/core/java/com/android/server/pm/dex/DexoptOptions.java @@ -18,6 +18,16 @@ package com.android.server.pm.dex; import static com.android.server.pm.PackageManagerServiceCompilerMapping.getCompilerFilterForReason; +import android.annotation.Nullable; + +import com.android.server.art.ReasonMapping; +import com.android.server.art.model.ArtFlags; +import com.android.server.art.model.OptimizeParams; +import com.android.server.pm.DexOptHelper; +import com.android.server.pm.PackageManagerService; + +import dalvik.system.DexFile; + /** * Options used for dexopt invocations. */ @@ -40,10 +50,6 @@ public final class DexoptOptions { // will only consider the primary apk. public static final int DEXOPT_ONLY_SECONDARY_DEX = 1 << 3; - // When set, dexopt will optimize only dex files that are used by other apps. - // Currently, this flag is ignored for primary apks. - public static final int DEXOPT_ONLY_SHARED_DEX = 1 << 4; - // When set, dexopt will attempt to scale down the optimizations previously applied in order // save disk space. public static final int DEXOPT_DOWNGRADE = 1 << 5; @@ -105,7 +111,6 @@ public final class DexoptOptions { DEXOPT_FORCE | DEXOPT_BOOT_COMPLETE | DEXOPT_ONLY_SECONDARY_DEX | - DEXOPT_ONLY_SHARED_DEX | DEXOPT_DOWNGRADE | DEXOPT_AS_SHARED_LIBRARY | DEXOPT_IDLE_BACKGROUND_JOB | @@ -146,10 +151,6 @@ public final class DexoptOptions { return (mFlags & DEXOPT_ONLY_SECONDARY_DEX) != 0; } - public boolean isDexoptOnlySharedDex() { - return (mFlags & DEXOPT_ONLY_SHARED_DEX) != 0; - } - public boolean isDowngrade() { return (mFlags & DEXOPT_DOWNGRADE) != 0; } @@ -198,4 +199,133 @@ public final class DexoptOptions { mSplitName, mFlags); } + + /** + * Returns an {@link OptimizeParams} instance corresponding to this object, for use with + * {@link com.android.server.art.ArtManagerLocal}. + * + * @param extraFlags extra {@link ArtFlags#OptimizeFlags} to set in the returned + * {@code OptimizeParams} beyond those converted from this object + * @return null if the settings cannot be accurately represented, and hence the old + * PackageManager/installd code paths need to be used. + */ + public @Nullable OptimizeParams convertToOptimizeParams(/*@OptimizeFlags*/ int extraFlags) { + if (mSplitName != null) { + DexOptHelper.reportArtManagerFallback( + mPackageName, "Request to optimize only split " + mSplitName); + return null; + } + + /*@OptimizeFlags*/ int flags = extraFlags; + if ((mFlags & DEXOPT_CHECK_FOR_PROFILES_UPDATES) == 0 + && DexFile.isProfileGuidedCompilerFilter(mCompilerFilter)) { + // ART Service doesn't support bypassing this, so not setting this flag is not + // supported. + DexOptHelper.reportArtManagerFallback(mPackageName, + "DEXOPT_CHECK_FOR_PROFILES_UPDATES not set with profile compiler filter"); + return null; + } + if ((mFlags & DEXOPT_FORCE) != 0) { + flags |= ArtFlags.FLAG_FORCE; + } + if ((mFlags & DEXOPT_ONLY_SECONDARY_DEX) != 0) { + flags |= ArtFlags.FLAG_FOR_SECONDARY_DEX; + } else { + flags |= ArtFlags.FLAG_FOR_PRIMARY_DEX; + } + if ((mFlags & DEXOPT_DOWNGRADE) != 0) { + flags |= ArtFlags.FLAG_SHOULD_DOWNGRADE; + } + if ((mFlags & DEXOPT_INSTALL_WITH_DEX_METADATA_FILE) == 0) { + // ART Service cannot be instructed to ignore a DM file if present, so not setting this + // flag is not supported. + DexOptHelper.reportArtManagerFallback( + mPackageName, "DEXOPT_INSTALL_WITH_DEX_METADATA_FILE not set"); + return null; + } + + /*@PriorityClassApi*/ int priority; + // Replicates logic in RunDex2Oat::PrepareCompilerRuntimeAndPerfConfigFlags in installd. + if ((mFlags & DEXOPT_BOOT_COMPLETE) != 0) { + if ((mFlags & DEXOPT_FOR_RESTORE) != 0) { + priority = ArtFlags.PRIORITY_INTERACTIVE_FAST; + } else { + // TODO(b/251903639): Repurpose DEXOPT_IDLE_BACKGROUND_JOB to choose new + // dalvik.vm.background-dex2oat-* properties. + priority = ArtFlags.PRIORITY_INTERACTIVE; + } + } else { + priority = ArtFlags.PRIORITY_BOOT; + } + + // The following flags in mFlags are ignored: + // + // - DEXOPT_AS_SHARED_LIBRARY: It's implicit with ART Service since it always looks at + // <uses-library> rather than actual dependencies. + // + // We don't require it to be set either. It's safe when switching between old and new + // code paths since the only effect is that some packages may be unnecessarily compiled + // without user profiles. + // + // - DEXOPT_IDLE_BACKGROUND_JOB: Its only effect is to allow the debug variant dex2oatd to + // be used, but ART Service never uses that (cf. Artd::GetDex2Oat in artd.cc). + + String reason; + switch (mCompilationReason) { + case PackageManagerService.REASON_FIRST_BOOT: + reason = ReasonMapping.REASON_FIRST_BOOT; + break; + case PackageManagerService.REASON_BOOT_AFTER_OTA: + reason = ReasonMapping.REASON_BOOT_AFTER_OTA; + break; + case PackageManagerService.REASON_POST_BOOT: + // This reason will go away with the legacy dexopt code. + DexOptHelper.reportArtManagerFallback( + mPackageName, "Unsupported compilation reason REASON_POST_BOOT"); + return null; + case PackageManagerService.REASON_INSTALL: + reason = ReasonMapping.REASON_INSTALL; + break; + case PackageManagerService.REASON_INSTALL_FAST: + reason = ReasonMapping.REASON_INSTALL_FAST; + break; + case PackageManagerService.REASON_INSTALL_BULK: + reason = ReasonMapping.REASON_INSTALL_BULK; + break; + case PackageManagerService.REASON_INSTALL_BULK_SECONDARY: + reason = ReasonMapping.REASON_INSTALL_BULK_SECONDARY; + break; + case PackageManagerService.REASON_INSTALL_BULK_DOWNGRADED: + reason = ReasonMapping.REASON_INSTALL_BULK_DOWNGRADED; + break; + case PackageManagerService.REASON_INSTALL_BULK_SECONDARY_DOWNGRADED: + reason = ReasonMapping.REASON_INSTALL_BULK_SECONDARY_DOWNGRADED; + break; + case PackageManagerService.REASON_BACKGROUND_DEXOPT: + reason = ReasonMapping.REASON_BG_DEXOPT; + break; + case PackageManagerService.REASON_INACTIVE_PACKAGE_DOWNGRADE: + reason = ReasonMapping.REASON_INACTIVE; + break; + case PackageManagerService.REASON_CMDLINE: + reason = ReasonMapping.REASON_CMDLINE; + break; + case PackageManagerService.REASON_SHARED: + case PackageManagerService.REASON_AB_OTA: + // REASON_SHARED shouldn't go into this code path - it's only used at lower levels + // in PackageDexOptimizer. + // TODO(b/251921228): OTA isn't supported, so REASON_AB_OTA shouldn't come this way + // either. + throw new UnsupportedOperationException( + "ART Service unsupported compilation reason " + mCompilationReason); + default: + throw new IllegalArgumentException( + "Invalid compilation reason " + mCompilationReason); + } + + return new OptimizeParams.Builder(reason, flags) + .setCompilerFilter(mCompilerFilter) + .setPriorityClass(priority) + .build(); + } } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index d39b64936bb3..3e0533f1e14b 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -4193,7 +4193,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mRequestedOrSleepingDefaultDisplay) { mCameraGestureTriggeredDuringGoingToSleep = true; // Wake device up early to prevent display doing redundant turning off/on stuff. - wakeUpFromPowerKey(event.getDownTime()); + wakeUp(SystemClock.uptimeMillis(), mAllowTheaterModeWakeFromPowerKey, + PowerManager.WAKE_REASON_CAMERA_LAUNCH, + "android.policy:CAMERA_GESTURE_PREVENT_LOCK"); } return true; } @@ -4726,11 +4728,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } mDefaultDisplayRotation.updateOrientationListener(); reportScreenStateToVrManager(false); - if (mCameraGestureTriggeredDuringGoingToSleep) { - wakeUp(SystemClock.uptimeMillis(), mAllowTheaterModeWakeFromPowerKey, - PowerManager.WAKE_REASON_CAMERA_LAUNCH, - "com.android.systemui:CAMERA_GESTURE_PREVENT_LOCK"); - } } } diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 5abc875a697f..d8b1120c624d 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -42,8 +42,6 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.SynchronousUserSwitchObserver; -import android.compat.annotation.ChangeId; -import android.compat.annotation.EnabledSince; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; @@ -64,7 +62,6 @@ import android.os.BatteryManager; import android.os.BatteryManagerInternal; import android.os.BatterySaverPolicyConfig; import android.os.Binder; -import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; import android.os.IBinder; @@ -127,7 +124,6 @@ import com.android.server.UiThread; import com.android.server.UserspaceRebootLogger; import com.android.server.Watchdog; import com.android.server.am.BatteryStatsService; -import com.android.server.compat.PlatformCompat; import com.android.server.lights.LightsManager; import com.android.server.lights.LogicalLight; import com.android.server.policy.WindowManagerPolicy; @@ -284,17 +280,6 @@ public final class PowerManagerService extends SystemService */ private static final long ENHANCED_DISCHARGE_PREDICTION_BROADCAST_MIN_DELAY_MS = 60 * 1000L; - /** - * Apps targeting Android U and above need to define - * {@link android.Manifest.permission#TURN_SCREEN_ON} in their manifest for - * {@link android.os.PowerManager#ACQUIRE_CAUSES_WAKEUP} to have any effect. - * Note that most applications should use {@link android.R.attr#turnScreenOn} or - * {@link android.app.Activity#setTurnScreenOn(boolean)} instead, as this prevents the - * previous foreground app from being resumed first when the screen turns on. - */ - @ChangeId - @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - public static final long REQUIRE_TURN_SCREEN_ON_PERMISSION = 216114297L; /** Reason ID for holding display suspend blocker. */ private static final String HOLDING_DISPLAY_SUSPEND_BLOCKER = "holding display"; @@ -318,7 +303,6 @@ public final class PowerManagerService extends SystemService private final SystemPropertiesWrapper mSystemProperties; private final Clock mClock; private final Injector mInjector; - private final PlatformCompat mPlatformCompat; private AppOpsManager mAppOpsManager; private LightsManager mLightsManager; @@ -1012,11 +996,6 @@ public final class PowerManagerService extends SystemService public void set(String key, String val) { SystemProperties.set(key, val); } - - @Override - public boolean getBoolean(String key, boolean def) { - return SystemProperties.getBoolean(key, def); - } }; } @@ -1053,10 +1032,6 @@ public final class PowerManagerService extends SystemService AppOpsManager createAppOpsManager(Context context) { return context.getSystemService(AppOpsManager.class); } - - PlatformCompat createPlatformCompat(Context context) { - return context.getSystemService(PlatformCompat.class); - } } final Constants mConstants; @@ -1114,8 +1089,6 @@ public final class PowerManagerService extends SystemService mAppOpsManager = injector.createAppOpsManager(mContext); - mPlatformCompat = injector.createPlatformCompat(mContext); - mPowerGroupWakefulnessChangeListener = new PowerGroupWakefulnessChangeListener(); // Save brightness values: @@ -1626,28 +1599,14 @@ public final class PowerManagerService extends SystemService } if (mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, opUid, opPackageName) == AppOpsManager.MODE_ALLOWED) { - if (mPlatformCompat.isChangeEnabledByPackageName(REQUIRE_TURN_SCREEN_ON_PERMISSION, - opPackageName, UserHandle.getUserId(opUid))) { - if (mContext.checkCallingOrSelfPermission( - android.Manifest.permission.TURN_SCREEN_ON) - == PackageManager.PERMISSION_GRANTED) { - if (DEBUG_SPEW) { - Slog.d(TAG, "Allowing device wake-up from app " + opPackageName); - } - return true; - } - } else { - // android.permission.TURN_SCREEN_ON has only been introduced in Android U, only - // check for appOp for apps targeting lower SDK versions - if (DEBUG_SPEW) { - Slog.d(TAG, "Allowing device wake-up from app with " - + "REQUIRE_TURN_SCREEN_ON_PERMISSION disabled " + opPackageName); - } + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.TURN_SCREEN_ON) + == PackageManager.PERMISSION_GRANTED) { + Slog.i(TAG, "Allowing device wake-up from app " + opPackageName); return true; } } - if (PowerProperties.permissionless_turn_screen_on().orElse(true)) { - Slog.d(TAG, "Device wake-up will be denied without android.permission.TURN_SCREEN_ON"); + if (PowerProperties.permissionless_turn_screen_on().orElse(false)) { + Slog.d(TAG, "Device wake-up allowed by debug.power.permissionless_turn_screen_on"); return true; } Slog.w(TAG, "Not allowing device wake-up for " + opPackageName); @@ -6768,6 +6727,11 @@ public final class PowerManagerService extends SystemService public void nap(long eventTime, boolean allowWake) { napInternal(eventTime, Process.SYSTEM_UID, allowWake); } + + @Override + public boolean isAmbientDisplaySuppressed() { + return mAmbientDisplaySuppressionController.isSuppressed(); + } } /** diff --git a/services/core/java/com/android/server/power/SystemPropertiesWrapper.java b/services/core/java/com/android/server/power/SystemPropertiesWrapper.java index c68f9c63b13b..1acf798eb099 100644 --- a/services/core/java/com/android/server/power/SystemPropertiesWrapper.java +++ b/services/core/java/com/android/server/power/SystemPropertiesWrapper.java @@ -48,19 +48,4 @@ interface SystemPropertiesWrapper { * SELinux. libc will log the underlying reason. */ void set(@NonNull String key, @Nullable String val); - - /** - * Get the value for the given {@code key}, returned as a boolean. - * Values 'n', 'no', '0', 'false' or 'off' are considered false. - * Values 'y', 'yes', '1', 'true' or 'on' are considered true. - * (case sensitive). - * If the key does not exist, or has any other value, then the default - * result is returned. - * - * @param key the key to lookup - * @param def a default value to return - * @return the key parsed as a boolean, or def if the key isn't found or is - * not able to be parsed as a boolean. - */ - boolean getBoolean(@NonNull String key, boolean def); } diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java index 8ac4fd4860bb..141be702ee13 100644 --- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java +++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java @@ -65,6 +65,9 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { public final DeviceVibrationEffectAdapter deviceEffectAdapter; public final VibrationThread.VibratorManagerHooks vibratorManagerHooks; + // Not guarded by lock because they're not modified by this conductor, it's used here only to + // check immutable attributes. The status and other mutable states are changed by the service or + // by the vibrator steps. private final Vibration mVibration; private final SparseArray<VibratorController> mVibrators = new SparseArray<>(); @@ -412,6 +415,16 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { } } + /** Returns true if a cancellation signal was sent via {@link #notifyCancelled}. */ + public boolean wasNotifiedToCancel() { + if (Build.IS_DEBUGGABLE) { + expectIsVibrationThread(false); + } + synchronized (mLock) { + return mSignalCancel != null; + } + } + @GuardedBy("mLock") private boolean hasPendingNotifySignalLocked() { if (Build.IS_DEBUGGABLE) { diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 8514e272e250..8613b5027d57 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -864,8 +864,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } Vibration currentVibration = mCurrentVibration.getVibration(); - if (currentVibration.hasEnded()) { - // Current vibration is finishing up, it should not block incoming vibrations. + if (currentVibration.hasEnded() || mCurrentVibration.wasNotifiedToCancel()) { + // Current vibration has ended or is cancelling, should not block incoming vibrations. return null; } diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 214a2c197a5c..3c457e1cc277 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2578,6 +2578,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { // activity lifecycle transaction to make sure the override pending app // transition will be applied immediately. targetActivity.applyOptionsAnimation(); + if (activityOptions != null && activityOptions.getLaunchCookie() != null) { + targetActivity.mLaunchCookie = activityOptions.getLaunchCookie(); + } } finally { mActivityMetricsLogger.notifyActivityLaunched(launchingState, START_TASK_TO_FRONT, false /* newActivityCreated */, diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index e9774479233d..30399ed15f7e 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -588,6 +588,7 @@ class BackNavigationController { ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Setting Activity.mLauncherTaskBehind to true. Activity=%s", activity); + activity.mTaskSupervisor.mStoppingActivities.remove(activity); activity.getDisplayContent().ensureActivitiesVisible(null /* starting */, 0 /* configChanges */, false /* preserveWindows */, true); } diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 32feb6c98b24..c206a15503de 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -613,15 +613,6 @@ public abstract class WindowManagerInternal { @NonNull IBinder imeTargetWindowToken); /** - * Returns the presence of a software navigation bar on the specified display. - * - * @param displayId the id of display to check if there is a software navigation bar. - * @return {@code true} if there is a software navigation. {@code false} otherwise, including - * the case when the specified display does not exist. - */ - public abstract boolean hasNavigationBar(int displayId); - - /** * Returns true when the hardware keyboard is available. */ public abstract boolean isHardKeyboardAvailable(); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index c17af3093e51..c9d3dac104de 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -7917,11 +7917,6 @@ public class WindowManagerService extends IWindowManager.Stub } @Override - public boolean hasNavigationBar(int displayId) { - return WindowManagerService.this.hasNavigationBar(displayId); - } - - @Override public boolean isHardKeyboardAvailable() { synchronized (mGlobalLock) { return mHardKeyboardAvailable; @@ -8703,11 +8698,12 @@ public class WindowManagerService extends IWindowManager.Stub h.ownerPid = callingPid; if (region == null) { - h.replaceTouchableRegionWithCrop = true; + h.replaceTouchableRegionWithCrop(null); } else { h.touchableRegion.set(region); + h.replaceTouchableRegionWithCrop = false; + h.setTouchableRegionCrop(surface); } - h.setTouchableRegionCrop(null /* use the input surface's bounds */); final SurfaceControl.Transaction t = mTransactionFactory.get(); t.setInputWindowInfo(surface, h); diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java index f45f62682f1c..aa19241e77dd 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java @@ -17,6 +17,11 @@ package com.android.server.credentials; import android.annotation.NonNull; +import android.app.AppGlobals; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.os.RemoteException; import android.util.Log; import com.android.server.infra.AbstractPerUserSystemService; @@ -24,7 +29,7 @@ import com.android.server.infra.AbstractPerUserSystemService; /** * Per-user implementation of {@link CredentialManagerService} */ -public class CredentialManagerServiceImpl extends +public final class CredentialManagerServiceImpl extends AbstractPerUserSystemService<CredentialManagerServiceImpl, CredentialManagerService> { private static final String TAG = "CredManSysServiceImpl"; @@ -34,6 +39,20 @@ public class CredentialManagerServiceImpl extends super(master, lock, userId); } + @Override // from PerUserSystemService + protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) + throws PackageManager.NameNotFoundException { + ServiceInfo si; + try { + si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent, + PackageManager.GET_META_DATA, mUserId); + } catch (RemoteException e) { + throw new PackageManager.NameNotFoundException( + "Could not get service for " + serviceComponent); + } + return si; + } + /** * Unimplemented getCredentials */ diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index 90b1f4ecdcb3..b7e66f23a706 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -39,6 +39,8 @@ import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.content.Intent; import android.content.IntentFilter; +import android.media.AudioManager; +import android.os.Bundle; import android.os.HandlerThread; import android.os.UserHandle; import android.provider.Settings; @@ -289,20 +291,30 @@ public class BroadcastQueueModernImplTest { final BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants, PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN)); + // enqueue a bg-priority broadcast then a fg-priority one + final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED); + final BroadcastRecord timezoneRecord = makeBroadcastRecord(timezone); + queue.enqueueOrReplaceBroadcast(timezoneRecord, 0, 0); + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane); queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, 0); + // verify that: + // (a) the queue is immediately runnable by existence of a fg-priority broadcast + // (b) the next one up is the fg-priority broadcast despite its later enqueue time queue.setProcessCached(false); assertTrue(queue.isRunnable()); assertEquals(airplaneRecord.enqueueTime, queue.getRunnableAt()); assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked()); + assertEquals(queue.peekNextBroadcastRecord(), airplaneRecord); queue.setProcessCached(true); assertTrue(queue.isRunnable()); assertEquals(airplaneRecord.enqueueTime, queue.getRunnableAt()); assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked()); + assertEquals(queue.peekNextBroadcastRecord(), airplaneRecord); } /** @@ -386,4 +398,86 @@ public class BroadcastQueueModernImplTest { assertEquals(Intent.ACTION_SCREEN_OFF, queue.getActive().intent.getAction()); assertTrue(queue.isEmpty()); } + + /** + * Verify that sending a broadcast with DELIVERY_GROUP_POLICY_MOST_RECENT works as expected. + */ + @Test + public void testDeliveryGroupPolicy_mostRecent() { + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); + final BroadcastOptions optionsTimeTick = BroadcastOptions.makeBasic(); + optionsTimeTick.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); + + final Intent musicVolumeChanged = new Intent(AudioManager.VOLUME_CHANGED_ACTION); + musicVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, + AudioManager.STREAM_MUSIC); + final BroadcastOptions optionsMusicVolumeChanged = BroadcastOptions.makeBasic(); + optionsMusicVolumeChanged.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); + optionsMusicVolumeChanged.setDeliveryGroupKey("audio", + String.valueOf(AudioManager.STREAM_MUSIC)); + + final Intent alarmVolumeChanged = new Intent(AudioManager.VOLUME_CHANGED_ACTION); + alarmVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, + AudioManager.STREAM_ALARM); + final BroadcastOptions optionsAlarmVolumeChanged = BroadcastOptions.makeBasic(); + optionsAlarmVolumeChanged.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); + optionsAlarmVolumeChanged.setDeliveryGroupKey("audio", + String.valueOf(AudioManager.STREAM_ALARM)); + + // Halt all processing so that we get a consistent view + mHandlerThread.getLooper().getQueue().postSyncBarrier(); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged, + optionsMusicVolumeChanged)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged, + optionsAlarmVolumeChanged)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged, + optionsMusicVolumeChanged)); + + final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + // Verify that the older musicVolumeChanged has been removed. + verifyPendingRecords(queue, + List.of(timeTick, alarmVolumeChanged, musicVolumeChanged)); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged, + optionsAlarmVolumeChanged)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged, + optionsMusicVolumeChanged)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged, + optionsAlarmVolumeChanged)); + // Verify that the older alarmVolumeChanged has been removed. + verifyPendingRecords(queue, + List.of(timeTick, musicVolumeChanged, alarmVolumeChanged)); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged, + optionsMusicVolumeChanged)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged, + optionsAlarmVolumeChanged)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick)); + // Verify that the older timeTick has been removed. + verifyPendingRecords(queue, + List.of(musicVolumeChanged, alarmVolumeChanged, timeTick)); + } + + private void verifyPendingRecords(BroadcastProcessQueue queue, + List<Intent> intents) { + for (int i = 0; i < intents.size(); i++) { + queue.makeActiveNextPending(); + final Intent actualIntent = queue.getActive().intent; + final Intent expectedIntent = intents.get(i); + final String errMsg = "actual=" + actualIntent + ", expected=" + expectedIntent + + ", actual_extras=" + actualIntent.getExtras() + + ", expected_extras=" + expectedIntent.getExtras(); + assertTrue(errMsg, actualIntent.filterEquals(expectedIntent)); + assertTrue(errMsg, Bundle.kindofEquals( + actualIntent.getExtras(), expectedIntent.getExtras())); + } + assertTrue(queue.isEmpty()); + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index c12544897941..d9a26c68f3ed 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -549,12 +549,6 @@ public class BroadcastQueueTest { receivers, false, null, null, userId); } - private BroadcastRecord makeOrderedBroadcastRecord(Intent intent, ProcessRecord callerApp, - List<Object> receivers, IIntentReceiver orderedResultTo, Bundle orderedExtras) { - return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(), - receivers, true, orderedResultTo, orderedExtras, UserHandle.USER_SYSTEM); - } - private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp, BroadcastOptions options, List<Object> receivers) { return makeBroadcastRecord(intent, callerApp, options, @@ -562,12 +556,24 @@ public class BroadcastQueueTest { } private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp, + List<Object> receivers, IIntentReceiver resultTo) { + return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(), + receivers, false, resultTo, null, UserHandle.USER_SYSTEM); + } + + private BroadcastRecord makeOrderedBroadcastRecord(Intent intent, ProcessRecord callerApp, + List<Object> receivers, IIntentReceiver resultTo, Bundle resultExtras) { + return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(), + receivers, true, resultTo, resultExtras, UserHandle.USER_SYSTEM); + } + + private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp, BroadcastOptions options, List<Object> receivers, boolean ordered, - IIntentReceiver orderedResultTo, Bundle orderedExtras, int userId) { + IIntentReceiver resultTo, Bundle resultExtras, int userId) { return new BroadcastRecord(mQueue, intent, callerApp, callerApp.info.packageName, null, callerApp.getPid(), callerApp.info.uid, false, null, null, null, null, - AppOpsManager.OP_NONE, options, receivers, callerApp, orderedResultTo, - Activity.RESULT_OK, null, orderedExtras, ordered, false, false, userId, false, null, + AppOpsManager.OP_NONE, options, receivers, callerApp, resultTo, + Activity.RESULT_OK, null, resultExtras, ordered, false, false, userId, false, null, false, null); } @@ -1347,6 +1353,26 @@ public class BroadcastQueueTest { } /** + * Verify that we deliver results for unordered broadcasts. + */ + @Test + public void testUnordered_ResultTo() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final IApplicationThread callerThread = callerApp.getThread(); + + final IIntentReceiver resultTo = mock(IIntentReceiver.class); + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, + List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), + makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE)), resultTo)); + + waitForIdle(); + verify(callerThread).scheduleRegisteredReceiver(any(), argThat(filterEquals(airplane)), + eq(Activity.RESULT_OK), any(), any(), eq(false), + anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt()); + } + + /** * Verify that we're not surprised by a process attempting to finishing a * broadcast when none is in progress. */ diff --git a/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java index 581a2a71a9b6..2d7d46f83c47 100644 --- a/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java +++ b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import android.app.backup.BackupTransport; +import android.app.backup.IBackupManagerMonitor; import android.app.backup.RestoreDescription; import android.app.backup.RestoreSet; import android.content.Intent; @@ -254,6 +255,9 @@ public class BackupTransportClientTest { ITransportStatusCallback c) throws RemoteException {} @Override public void abortFullRestore(ITransportStatusCallback c) throws RemoteException {} @Override public void getTransportFlags(AndroidFuture<Integer> f) throws RemoteException {} + @Override + public void getBackupManagerMonitor(AndroidFuture<IBackupManagerMonitor> resultFuture) + throws RemoteException {} @Override public IBinder asBinder() { return null; } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java index eb1314194aa3..ffacbf331d89 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java @@ -16,7 +16,7 @@ package com.android.server.biometrics.sensors; -import static android.testing.TestableLooper.RunWithLooper; +import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; @@ -24,8 +24,10 @@ import static junit.framework.Assert.fail; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -35,6 +37,7 @@ import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; import android.content.Context; +import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.IBiometricService; import android.os.Binder; @@ -63,28 +66,26 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; import java.util.function.Supplier; @Presubmit @SmallTest @RunWith(AndroidTestingRunner.class) -@RunWithLooper(setAsMainLooper = true) +@TestableLooper.RunWithLooper(setAsMainLooper = true) public class BiometricSchedulerTest { private static final String TAG = "BiometricSchedulerTest"; private static final int TEST_SENSOR_ID = 1; private static final int LOG_NUM_RECENT_OPERATIONS = 2; - + @Rule + public final TestableContext mContext = + new TestableContext(InstrumentationRegistry.getContext(), null); private BiometricScheduler mScheduler; private IBinder mToken; - @Mock private IBiometricService mBiometricService; - @Rule - public final TestableContext mContext = - new TestableContext(InstrumentationRegistry.getContext(), null); - @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -323,7 +324,7 @@ public class BiometricSchedulerTest { client1.getCallback().onClientFinished(client1, true /* success */); waitForIdle(); verify(callback).onError(anyInt(), anyInt(), - eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED), + eq(BIOMETRIC_ERROR_CANCELED), eq(0) /* vendorCode */); assertNull(mScheduler.getCurrentClient()); assertTrue(client1.isAlreadyDone()); @@ -484,7 +485,7 @@ public class BiometricSchedulerTest { mScheduler.scheduleClientMonitor(interrupter); waitForIdle(); - verify((Interruptable) interruptableMonitor).cancel(); + verify(interruptableMonitor).cancel(); mScheduler.getInternalCallback().onClientFinished(interruptableMonitor, true /* success */); } @@ -500,7 +501,7 @@ public class BiometricSchedulerTest { mScheduler.scheduleClientMonitor(interrupter); waitForIdle(); - verify((Interruptable) interruptableMonitor, never()).cancel(); + verify(interruptableMonitor, never()).cancel(); } @Test @@ -514,21 +515,180 @@ public class BiometricSchedulerTest { assertTrue(client.mDestroyed); } + @Test + public void testClearBiometricQueue_clearsHungAuthOperation() { + // Creating a hung client + final TestableLooper looper = TestableLooper.get(this); + final Supplier<Object> lazyDaemon1 = () -> mock(Object.class); + final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext, + lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */); + final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class); + + mScheduler.scheduleClientMonitor(client1, callback1); + waitForIdle(); + + mScheduler.startWatchdog(); + waitForIdle(); + + //Checking client is hung + verify(callback1).onClientStarted(client1); + verify(callback1, never()).onClientFinished(any(), anyBoolean()); + assertNotNull(mScheduler.mCurrentOperation); + assertEquals(0, mScheduler.getCurrentPendingCount()); + + looper.moveTimeForward(10000); + waitForIdle(); + looper.moveTimeForward(3000); + waitForIdle(); + + // The hung client did not honor this operation, verify onError and authenticated + // were never called. + assertFalse(client1.mOnErrorCalled); + assertFalse(client1.mAuthenticateCalled); + verify(callback1).onClientFinished(client1, false /* success */); + assertNull(mScheduler.mCurrentOperation); + assertEquals(0, mScheduler.getCurrentPendingCount()); + } + + @Test + public void testAuthWorks_afterClearBiometricQueue() { + // Creating a hung client + final TestableLooper looper = TestableLooper.get(this); + final Supplier<Object> lazyDaemon1 = () -> mock(Object.class); + final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext, + lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */); + final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class); + + mScheduler.scheduleClientMonitor(client1, callback1); + + assertEquals(client1, mScheduler.mCurrentOperation.getClientMonitor()); + assertEquals(0, mScheduler.getCurrentPendingCount()); + + //Checking client is hung + waitForIdle(); + verify(callback1, never()).onClientFinished(any(), anyBoolean()); + + //Start watchdog + mScheduler.startWatchdog(); + waitForIdle(); + + // The watchdog should kick off the cancellation + looper.moveTimeForward(10000); + waitForIdle(); + // After 10 seconds the HAL has 3 seconds to respond to a cancel + looper.moveTimeForward(3000); + waitForIdle(); + + // The hung client did not honor this operation, verify onError and authenticated + // were never called. + assertFalse(client1.mOnErrorCalled); + assertFalse(client1.mAuthenticateCalled); + verify(callback1).onClientFinished(client1, false /* success */); + assertEquals(0, mScheduler.getCurrentPendingCount()); + assertNull(mScheduler.mCurrentOperation); + + + //Run additional auth client + final TestAuthenticationClient client2 = new TestAuthenticationClient(mContext, + lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */); + final ClientMonitorCallback callback2 = mock(ClientMonitorCallback.class); + + mScheduler.scheduleClientMonitor(client2, callback2); + + assertEquals(client2, mScheduler.mCurrentOperation.getClientMonitor()); + assertEquals(0, mScheduler.getCurrentPendingCount()); + + //Start watchdog + mScheduler.startWatchdog(); + waitForIdle(); + mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class), + mock(ClientMonitorCallback.class)); + waitForIdle(); + + //Ensure auth client passes + verify(callback2).onClientStarted(client2); + client2.getCallback().onClientFinished(client2, true); + waitForIdle(); + + looper.moveTimeForward(10000); + waitForIdle(); + // After 10 seconds the HAL has 3 seconds to respond to a cancel + looper.moveTimeForward(3000); + waitForIdle(); + + //Asserting auth client passes + assertTrue(client2.isAlreadyDone()); + assertNotNull(mScheduler.mCurrentOperation); + } + + @Test + public void testClearBiometricQueue_doesNotClearOperationsWhenQueueNotStuck() { + //Creating clients + final TestableLooper looper = TestableLooper.get(this); + final Supplier<Object> lazyDaemon1 = () -> mock(Object.class); + final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext, + lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */); + final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class); + + mScheduler.scheduleClientMonitor(client1, callback1); + //Start watchdog + mScheduler.startWatchdog(); + waitForIdle(); + mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class), + mock(ClientMonitorCallback.class)); + mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class), + mock(ClientMonitorCallback.class)); + waitForIdle(); + + assertEquals(client1, mScheduler.mCurrentOperation.getClientMonitor()); + assertEquals(2, mScheduler.getCurrentPendingCount()); + verify(callback1, never()).onClientFinished(any(), anyBoolean()); + verify(callback1).onClientStarted(client1); + + //Client finishes successfully + client1.getCallback().onClientFinished(client1, true); + waitForIdle(); + + // The watchdog should kick off the cancellation + looper.moveTimeForward(10000); + waitForIdle(); + // After 10 seconds the HAL has 3 seconds to respond to a cancel + looper.moveTimeForward(3000); + waitForIdle(); + + //Watchdog does not clear pending operations + assertEquals(1, mScheduler.getCurrentPendingCount()); + assertNotNull(mScheduler.mCurrentOperation); + + } + private BiometricSchedulerProto getDump(boolean clearSchedulerBuffer) throws Exception { return BiometricSchedulerProto.parseFrom(mScheduler.dumpProtoState(clearSchedulerBuffer)); } + private void waitForIdle() { + TestableLooper.get(this).processAllMessages(); + } + private static class TestAuthenticationClient extends AuthenticationClient<Object> { boolean mStartedHal = false; boolean mStoppedHal = false; boolean mDestroyed = false; int mNumCancels = 0; + boolean mAuthenticateCalled = false; + boolean mOnErrorCalled = false; - public TestAuthenticationClient(@NonNull Context context, + TestAuthenticationClient(@NonNull Context context, @NonNull Supplier<Object> lazyDaemon, @NonNull IBinder token, @NonNull ClientMonitorCallbackConverter listener) { + this(context, lazyDaemon, token, listener, 1 /* cookie */); + } + + TestAuthenticationClient(@NonNull Context context, + @NonNull Supplier<Object> lazyDaemon, @NonNull IBinder token, + @NonNull ClientMonitorCallbackConverter listener, int cookie) { super(context, lazyDaemon, token, listener, 0 /* targetUserId */, 0 /* operationId */, - false /* restricted */, TAG, 1 /* cookie */, false /* requireConfirmation */, + false /* restricted */, TAG, cookie, false /* requireConfirmation */, TEST_SENSOR_ID, mock(BiometricLogger.class), mock(BiometricContext.class), true /* isStrongBiometric */, null /* taskStackListener */, mock(LockoutTracker.class), false /* isKeyguard */, @@ -546,7 +706,19 @@ public class BiometricSchedulerTest { } @Override - protected void handleLifecycleAfterAuth(boolean authenticated) {} + protected void handleLifecycleAfterAuth(boolean authenticated) { + } + + @Override + public void onAuthenticated(BiometricAuthenticator.Identifier identifier, + boolean authenticated, ArrayList<Byte> hardwareAuthToken) { + mAuthenticateCalled = true; + } + + @Override + protected void onErrorInternal(int errorCode, int vendorCode, boolean finish) { + mOnErrorCalled = true; + } @Override public boolean wasUserDetected() { @@ -651,8 +823,4 @@ public class BiometricSchedulerTest { mDestroyed = true; } } - - private void waitForIdle() { - TestableLooper.get(this).processAllMessages(); - } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index 73548a3a1132..1b5db0a35449 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -41,6 +41,7 @@ import android.hardware.biometrics.common.OperationContext; import android.hardware.biometrics.fingerprint.ISession; import android.hardware.biometrics.fingerprint.PointerContext; import android.hardware.fingerprint.Fingerprint; +import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.ISidefpsController; import android.hardware.fingerprint.IUdfpsOverlayController; @@ -74,6 +75,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.time.Clock; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -131,6 +133,8 @@ public class FingerprintAuthenticationClientTest { private Probe mLuxProbe; @Mock private AuthSessionCoordinator mAuthSessionCoordinator; + @Mock + private Clock mClock; @Captor private ArgumentCaptor<OperationContext> mOperationContextCaptor; @Captor @@ -451,6 +455,52 @@ public class FingerprintAuthenticationClientTest { } @Test + public void sideFingerprintSkipsWindowIfVendorMessageMatch() throws Exception { + when(mSensorProps.isAnySidefpsType()).thenReturn(true); + final int vendorAcquireMessage = 1234; + + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, + FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR); + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage, + vendorAcquireMessage); + + final FingerprintAuthenticationClient client = createClient(1); + client.start(mCallback); + mLooper.dispatchAll(); + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + true /* authenticated */, new ArrayList<>()); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, vendorAcquireMessage); + mLooper.dispatchAll(); + + verify(mCallback).onClientFinished(any(), eq(true)); + } + + @Test + public void sideFingerprintDoesNotSkipWindowOnVendorErrorMismatch() throws Exception { + when(mSensorProps.isAnySidefpsType()).thenReturn(true); + final int vendorAcquireMessage = 1234; + + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, + FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR); + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage, + vendorAcquireMessage); + + final FingerprintAuthenticationClient client = createClient(1); + client.start(mCallback); + mLooper.dispatchAll(); + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + true /* authenticated */, new ArrayList<>()); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, 1); + mLooper.dispatchAll(); + + verify(mCallback, never()).onClientFinished(any(), anyBoolean()); + } + + @Test public void sideFingerprintSendsAuthIfFingerUp() throws Exception { when(mSensorProps.isAnySidefpsType()).thenReturn(true); @@ -497,6 +547,79 @@ public class FingerprintAuthenticationClientTest { verify(mCallback).onClientFinished(any(), eq(true)); } + @Test + public void sideFingerprintPowerWindowStartsOnAcquireStart() throws Exception { + final int powerWindow = 500; + final long authStart = 300; + + when(mSensorProps.isAnySidefpsType()).thenReturn(true); + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsBpPowerPressWindow, powerWindow); + + final FingerprintAuthenticationClient client = createClient(1); + client.start(mCallback); + + // Acquire start occurs at time = 0ms + when(mClock.millis()).thenReturn(0L); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); + + // Auth occurs at time = 300 + when(mClock.millis()).thenReturn(authStart); + // At this point the delay should be 500 - (300 - 0) == 200 milliseconds. + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + true /* authenticated */, new ArrayList<>()); + mLooper.dispatchAll(); + verify(mCallback, never()).onClientFinished(any(), anyBoolean()); + + // After waiting 200 milliseconds, auth should succeed. + mLooper.moveTimeForward(powerWindow - authStart); + mLooper.dispatchAll(); + verify(mCallback).onClientFinished(any(), eq(true)); + } + + @Test + public void sideFingerprintPowerWindowStartsOnLastAcquireStart() throws Exception { + final int powerWindow = 500; + + when(mSensorProps.isAnySidefpsType()).thenReturn(true); + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsBpPowerPressWindow, powerWindow); + + final FingerprintAuthenticationClient client = createClient(1); + client.start(mCallback); + // Acquire start occurs at time = 0ms + when(mClock.millis()).thenReturn(0L); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); + + // Auth reject occurs at time = 300ms + when(mClock.millis()).thenReturn(300L); + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + false /* authenticated */, new ArrayList<>()); + mLooper.dispatchAll(); + + mLooper.moveTimeForward(300); + mLooper.dispatchAll(); + verify(mCallback, never()).onClientFinished(any(), anyBoolean()); + + when(mClock.millis()).thenReturn(1300L); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); + + // If code is correct, the new acquired start timestamp should be used + // and the code should only have to wait 500 - (1500-1300)ms. + when(mClock.millis()).thenReturn(1500L); + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + true /* authenticated */, new ArrayList<>()); + mLooper.dispatchAll(); + + mLooper.moveTimeForward(299); + mLooper.dispatchAll(); + verify(mCallback, never()).onClientFinished(any(), anyBoolean()); + + mLooper.moveTimeForward(1); + mLooper.dispatchAll(); + verify(mCallback).onClientFinished(any(), eq(true)); + } + private FingerprintAuthenticationClient createClient() throws RemoteException { return createClient(100 /* version */, true /* allowBackgroundAuthentication */); } @@ -524,7 +647,7 @@ public class FingerprintAuthenticationClientTest { null /* taskStackListener */, mLockoutCache, mUdfpsOverlayController, mSideFpsController, allowBackgroundAuthentication, mSensorProps, - new Handler(mLooper.getLooper()), 0 /* biometricStrength */) { + new Handler(mLooper.getLooper()), 0 /* biometricStrength */, mClock) { @Override protected ActivityTaskManager getActivityTaskManager() { return mActivityTaskManager; diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java index 6b8c26d7b1d4..d2f2af1b91b6 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java @@ -16,6 +16,8 @@ package com.android.server.companion.virtual; +import static com.google.common.truth.Truth.assertWithMessage; + import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -25,6 +27,7 @@ import static org.mockito.Mockito.verify; import android.hardware.display.DisplayManagerInternal; import android.hardware.input.IInputManager; +import android.hardware.input.InputManager; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -88,6 +91,30 @@ public class InputControllerTest { } @Test + public void registerInputDevice_deviceCreation_hasDeviceId() { + final IBinder device1Token = new Binder("device1"); + mInputController.createMouse("mouse", /*vendorId= */ 1, /*productId= */ 1, device1Token, + /* displayId= */ 1); + int device1Id = mInputController.getInputDeviceId(device1Token); + + final IBinder device2Token = new Binder("device2"); + mInputController.createKeyboard("keyboard", /*vendorId= */2, /*productId= */ 2, + device2Token, 2); + int device2Id = mInputController.getInputDeviceId(device2Token); + + assertWithMessage("Different devices should have different id").that( + device1Id).isNotEqualTo(device2Id); + + + int[] deviceIds = InputManager.getInstance().getInputDeviceIds(); + assertWithMessage("InputManager's deviceIds list should contain id of device 1").that( + deviceIds).asList().contains(device1Id); + assertWithMessage("InputManager's deviceIds list should contain id of device 2").that( + deviceIds).asList().contains(device2Id); + + } + + @Test public void unregisterInputDevice_allMiceUnregistered_clearPointerDisplayId() { final IBinder deviceToken = new Binder(); mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken, @@ -115,4 +142,5 @@ public class InputControllerTest { mInputController.unregisterInputDevice(deviceToken); verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1)); } + } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 9c5d1a5b0610..02bbe658f9b2 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -121,6 +121,7 @@ public class VirtualDeviceManagerServiceTest { private static final int VENDOR_ID = 5; private static final String UNIQUE_ID = "uniqueid"; private static final String PHYS = "phys"; + private static final int DEVICE_ID = 42; private static final int HEIGHT = 1800; private static final int WIDTH = 900; private static final Binder BINDER = new Binder("binder"); @@ -530,6 +531,16 @@ public class VirtualDeviceManagerServiceTest { } @Test + public void createVirtualKeyboard_inputDeviceId_obtainFromInputController() { + final int fd = 1; + mInputController.addDeviceForTesting(BINDER, fd, /* type= */ 1, /* displayId= */ 1, PHYS, + DEVICE_ID); + assertWithMessage( + "InputController should return device id from InputDeviceDescriptor").that( + mInputController.getInputDeviceId(BINDER)).isEqualTo(DEVICE_ID); + } + + @Test public void onAudioSessionStarting_hasVirtualAudioController() { mDeviceImpl.onVirtualDisplayCreatedLocked( mDeviceImpl.createWindowPolicyController(), DISPLAY_ID); @@ -576,9 +587,9 @@ public class VirtualDeviceManagerServiceTest { final int fd = 1; final int keyCode = KeyEvent.KEYCODE_A; final int action = VirtualKeyEvent.ACTION_UP; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 1, - /* displayId= */ 1, PHYS)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */1, /* displayId= */ 1, PHYS, + DEVICE_ID); + mDeviceImpl.sendKeyEvent(BINDER, new VirtualKeyEvent.Builder().setKeyCode(keyCode) .setAction(action).build()); verify(mNativeWrapperMock).writeKeyEvent(fd, keyCode, action); @@ -601,9 +612,8 @@ public class VirtualDeviceManagerServiceTest { final int fd = 1; final int buttonCode = VirtualMouseButtonEvent.BUTTON_BACK; final int action = VirtualMouseButtonEvent.ACTION_BUTTON_PRESS; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2, - /* displayId= */ 1, PHYS)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS, + DEVICE_ID); doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId(); mDeviceImpl.sendButtonEvent(BINDER, new VirtualMouseButtonEvent.Builder() .setButtonCode(buttonCode) @@ -616,9 +626,8 @@ public class VirtualDeviceManagerServiceTest { final int fd = 1; final int buttonCode = VirtualMouseButtonEvent.BUTTON_BACK; final int action = VirtualMouseButtonEvent.ACTION_BUTTON_PRESS; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2, - /* displayId= */ 1, PHYS)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS, + DEVICE_ID); assertThrows( IllegalStateException.class, () -> @@ -642,9 +651,8 @@ public class VirtualDeviceManagerServiceTest { final int fd = 1; final float x = -0.2f; final float y = 0.7f; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2, - /* displayId= */ 1, PHYS)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS, + DEVICE_ID); doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId(); mDeviceImpl.sendRelativeEvent(BINDER, new VirtualMouseRelativeEvent.Builder() .setRelativeX(x).setRelativeY(y).build()); @@ -656,9 +664,8 @@ public class VirtualDeviceManagerServiceTest { final int fd = 1; final float x = -0.2f; final float y = 0.7f; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2, - /* displayId= */ 1, PHYS)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS, + DEVICE_ID); assertThrows( IllegalStateException.class, () -> @@ -683,9 +690,8 @@ public class VirtualDeviceManagerServiceTest { final int fd = 1; final float x = 0.5f; final float y = 1f; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2, - /* displayId= */ 1, PHYS)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS, + DEVICE_ID); doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId(); mDeviceImpl.sendScrollEvent(BINDER, new VirtualMouseScrollEvent.Builder() .setXAxisMovement(x) @@ -698,9 +704,8 @@ public class VirtualDeviceManagerServiceTest { final int fd = 1; final float x = 0.5f; final float y = 1f; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2, - /* displayId= */ 1, PHYS)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS, + DEVICE_ID); assertThrows( IllegalStateException.class, () -> @@ -731,9 +736,8 @@ public class VirtualDeviceManagerServiceTest { final float x = 100.5f; final float y = 200.5f; final int action = VirtualTouchEvent.ACTION_UP; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 3, - /* displayId= */ 1, PHYS)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */3, /* displayId= */ 1, PHYS, + DEVICE_ID); mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder().setX(x) .setY(y).setAction(action).setPointerId(pointerId).setToolType(toolType).build()); verify(mNativeWrapperMock).writeTouchEvent(fd, pointerId, toolType, action, x, y, Float.NaN, @@ -750,9 +754,8 @@ public class VirtualDeviceManagerServiceTest { final int action = VirtualTouchEvent.ACTION_UP; final float pressure = 1.0f; final float majorAxisSize = 10.0f; - mInputController.mInputDeviceDescriptors.put(BINDER, - new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 3, - /* displayId= */ 1, PHYS)); + mInputController.addDeviceForTesting(BINDER, fd, /* type= */3, /* displayId= */ 1, PHYS, + DEVICE_ID); mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder().setX(x) .setY(y).setAction(action).setPointerId(pointerId).setToolType(toolType) .setPressure(pressure).setMajorAxisSize(majorAxisSize).build()); diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java index 6860abf40b56..062bde8f080b 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -18,6 +18,7 @@ package com.android.server.display; import static android.Manifest.permission.ADD_TRUSTED_DISPLAY; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; +import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP; import static com.android.server.display.VirtualDisplayAdapter.UNIQUE_ID_PREFIX; @@ -660,6 +661,117 @@ public class DisplayManagerServiceTest { firstDisplayId); } + /** Tests that the virtual device is created in a device display group. */ + @Test + public void createVirtualDisplay_addsDisplaysToDeviceDisplayGroups() throws Exception { + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + + registerDefaultDisplays(displayManager); + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + + when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)) + .thenReturn(PackageManager.PERMISSION_DENIED); + + IVirtualDevice virtualDevice = mock(IVirtualDevice.class); + when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice)) + .thenReturn(true); + when(virtualDevice.getDeviceId()).thenReturn(1); + + // Create a first virtual display. A display group should be created for this display on the + // virtual device. + final VirtualDisplayConfig.Builder builder1 = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setUniqueId("uniqueId --- device display group 1"); + + int displayId1 = + localService.createVirtualDisplay( + builder1.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId; + + // Create a second virtual display. This should be added to the previously created display + // group. + final VirtualDisplayConfig.Builder builder2 = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setUniqueId("uniqueId --- device display group 1"); + + int displayId2 = + localService.createVirtualDisplay( + builder2.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId; + + assertEquals( + "Both displays should be added to the same displayGroup.", + displayGroupId1, + displayGroupId2); + } + + /** + * Tests that the virtual display is not added to the device display group when + * VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is set. + */ + @Test + public void createVirtualDisplay_addsDisplaysToOwnDisplayGroups() throws Exception { + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + + registerDefaultDisplays(displayManager); + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + + when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)) + .thenReturn(PackageManager.PERMISSION_DENIED); + + IVirtualDevice virtualDevice = mock(IVirtualDevice.class); + when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice)) + .thenReturn(true); + when(virtualDevice.getDeviceId()).thenReturn(1); + + // Create a first virtual display. A display group should be created for this display on the + // virtual device. + final VirtualDisplayConfig.Builder builder1 = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setUniqueId("uniqueId --- device display group 1"); + + int displayId1 = + localService.createVirtualDisplay( + builder1.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId; + + // Create a second virtual display. With the flag VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP, + // the display should not be added to the previously created display group. + final VirtualDisplayConfig.Builder builder2 = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) + .setUniqueId("uniqueId --- device display group 1"); + + int displayId2 = + localService.createVirtualDisplay( + builder2.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId; + + assertNotEquals( + "Display 1 should be in the device display group and display 2 in its own display" + + " group.", + displayGroupId1, + displayGroupId2); + } + @Test public void testGetDisplayIdToMirror() throws Exception { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java index 0b33c30fd7e8..657bda633ab5 100644 --- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -369,6 +369,98 @@ public class LogicalDisplayMapperTest { } @Test + public void testDevicesAreAddedToDeviceDisplayGroups() { + // Create the default internal display of the device. + LogicalDisplay defaultDisplay = + add( + createDisplayDevice( + Display.TYPE_INTERNAL, + 600, + 800, + DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); + + // Create 3 virtual displays associated with a first virtual device. + int deviceId1 = 1; + TestDisplayDevice display1 = + createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display1", 600, 800, 0); + mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display1, deviceId1); + LogicalDisplay virtualDevice1Display1 = add(display1); + + TestDisplayDevice display2 = + createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display2", 600, 800, 0); + mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display2, deviceId1); + LogicalDisplay virtualDevice1Display2 = add(display2); + + TestDisplayDevice display3 = + createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display3", 600, 800, 0); + mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display3, deviceId1); + LogicalDisplay virtualDevice1Display3 = add(display3); + + // Create another 3 virtual displays associated with a second virtual device. + int deviceId2 = 2; + TestDisplayDevice display4 = + createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice2Display1", 600, 800, 0); + mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display4, deviceId2); + LogicalDisplay virtualDevice2Display1 = add(display4); + + TestDisplayDevice display5 = + createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice2Display2", 600, 800, 0); + mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display5, deviceId2); + LogicalDisplay virtualDevice2Display2 = add(display5); + + // The final display is created with FLAG_OWN_DISPLAY_GROUP set. + TestDisplayDevice display6 = + createDisplayDevice( + Display.TYPE_VIRTUAL, + "virtualDevice2Display3", + 600, + 800, + DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP); + mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display6, deviceId2); + LogicalDisplay virtualDevice2Display3 = add(display6); + + // Verify that the internal display is in the default display group. + assertEquals( + DEFAULT_DISPLAY_GROUP, + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(id(defaultDisplay))); + + // Verify that all the displays for virtual device 1 are in the same (non-default) display + // group. + int virtualDevice1DisplayGroupId = + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked( + id(virtualDevice1Display1)); + assertNotEquals(DEFAULT_DISPLAY_GROUP, virtualDevice1DisplayGroupId); + assertEquals( + virtualDevice1DisplayGroupId, + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked( + id(virtualDevice1Display2))); + assertEquals( + virtualDevice1DisplayGroupId, + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked( + id(virtualDevice1Display3))); + + // The first 2 displays for virtual device 2 should be in the same non-default group. + int virtualDevice2DisplayGroupId = + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked( + id(virtualDevice2Display1)); + assertNotEquals(DEFAULT_DISPLAY_GROUP, virtualDevice2DisplayGroupId); + assertEquals( + virtualDevice2DisplayGroupId, + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked( + id(virtualDevice2Display2))); + // virtualDevice2Display3 was created with FLAG_OWN_DISPLAY_GROUP and shouldn't be grouped + // with other displays of this device or be in the default display group. + assertNotEquals( + virtualDevice2DisplayGroupId, + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked( + id(virtualDevice2Display3))); + assertNotEquals( + DEFAULT_DISPLAY_GROUP, + mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked( + id(virtualDevice2Display3))); + } + + @Test public void testDeviceShouldBeWoken() { assertTrue(mLogicalDisplayMapper.shouldDeviceBeWoken(DEVICE_STATE_OPEN, DEVICE_STATE_CLOSED, @@ -416,14 +508,22 @@ public class LogicalDisplayMapperTest { ///////////////// private TestDisplayDevice createDisplayDevice(int type, int width, int height, int flags) { - return createDisplayDevice(new TestUtils.TestDisplayAddress(), type, width, height, flags); + return createDisplayDevice( + new TestUtils.TestDisplayAddress(), /* uniqueId */ "", type, width, height, flags); + } + + private TestDisplayDevice createDisplayDevice( + int type, String uniqueId, int width, int height, int flags) { + return createDisplayDevice( + new TestUtils.TestDisplayAddress(), uniqueId, type, width, height, flags); } private TestDisplayDevice createDisplayDevice( - DisplayAddress address, int type, int width, int height, int flags) { + DisplayAddress address, String uniqueId, int type, int width, int height, int flags) { TestDisplayDevice device = new TestDisplayDevice(); DisplayDeviceInfo displayDeviceInfo = device.getSourceInfo(); displayDeviceInfo.type = type; + displayDeviceInfo.uniqueId = uniqueId; displayDeviceInfo.width = width; displayDeviceInfo.height = height; displayDeviceInfo.flags = flags; diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt index 65076a372b0b..b095a506af21 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -32,6 +32,7 @@ import android.os.test.TestLooper import android.platform.test.annotations.Presubmit import android.view.InputDevice import androidx.test.InstrumentationRegistry +import com.android.server.input.BatteryController.POLLING_PERIOD_MILLIS import com.android.server.input.BatteryController.UEventManager import com.android.server.input.BatteryController.UEventManager.UEventBatteryListener import org.hamcrest.Description @@ -42,6 +43,8 @@ import org.hamcrest.TypeSafeMatcher import org.hamcrest.core.IsEqual.equalTo import org.junit.After import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Before import org.junit.Rule @@ -63,14 +66,20 @@ import org.mockito.hamcrest.MockitoHamcrest import org.mockito.junit.MockitoJUnit import org.mockito.verification.VerificationMode -private fun createInputDevice(deviceId: Int, hasBattery: Boolean = true): InputDevice = +private fun createInputDevice( + deviceId: Int, + hasBattery: Boolean = true, + supportsUsi: Boolean = false, + generation: Int = -1, +): InputDevice = InputDevice.Builder() .setId(deviceId) .setName("Device $deviceId") .setDescriptor("descriptor $deviceId") .setExternal(true) .setHasBattery(hasBattery) - .setGeneration(0) + .setSupportsUsi(supportsUsi) + .setGeneration(generation) .build() // Returns a matcher that helps match member variables of a class. @@ -118,7 +127,10 @@ private fun matchesState( return Matchers.allOf(batteryStateMatchers) } -// Helper used to verify interactions with a mocked battery listener. +private fun isInvalidBatteryState(deviceId: Int): Matcher<IInputDeviceBatteryState> = + matchesState(deviceId, isPresent = false, status = STATUS_UNKNOWN, capacity = Float.NaN) + +// Helpers used to verify interactions with a mocked battery listener. private fun IInputDeviceBatteryListener.verifyNotified( deviceId: Int, mode: VerificationMode = times(1), @@ -127,8 +139,21 @@ private fun IInputDeviceBatteryListener.verifyNotified( capacity: Float? = null, eventTime: Long? = null ) { - verify(this, mode).onBatteryStateChanged( - MockitoHamcrest.argThat(matchesState(deviceId, isPresent, status, capacity, eventTime))) + verifyNotified(matchesState(deviceId, isPresent, status, capacity, eventTime), mode) +} + +private fun IInputDeviceBatteryListener.verifyNotified( + matcher: Matcher<IInputDeviceBatteryState>, + mode: VerificationMode = times(1) +) { + verify(this, mode).onBatteryStateChanged(MockitoHamcrest.argThat(matcher)) +} + +private fun createMockListener(): IInputDeviceBatteryListener { + val listener = mock(IInputDeviceBatteryListener::class.java) + val binder = mock(Binder::class.java) + `when`(listener.asBinder()).thenReturn(binder) + return listener } /** @@ -143,6 +168,8 @@ class BatteryControllerTests { const val PID = 42 const val DEVICE_ID = 13 const val SECOND_DEVICE_ID = 11 + const val USI_DEVICE_ID = 101 + const val SECOND_USI_DEVICE_ID = 102 const val TIMESTAMP = 123456789L } @@ -168,10 +195,11 @@ class BatteryControllerTests { testLooper = TestLooper() val inputManager = InputManager.resetInstance(iInputManager) `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager) - `when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID, SECOND_DEVICE_ID)) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(createInputDevice(DEVICE_ID)) - `when`(iInputManager.getInputDevice(SECOND_DEVICE_ID)) - .thenReturn(createInputDevice(SECOND_DEVICE_ID)) + `when`(iInputManager.inputDeviceIds).then { + deviceGenerationMap.keys.toIntArray() + } + addInputDevice(DEVICE_ID) + addInputDevice(SECOND_DEVICE_ID) batteryController = BatteryController(context, native, testLooper.looper, uEventManager) batteryController.systemRunning() @@ -180,10 +208,30 @@ class BatteryControllerTests { devicesChangedListener = listenerCaptor.value } - private fun notifyDeviceChanged(deviceId: Int) { - deviceGenerationMap[deviceId] = deviceGenerationMap[deviceId]?.plus(1) ?: 1 + private fun notifyDeviceChanged( + deviceId: Int, + hasBattery: Boolean = true, + supportsUsi: Boolean = false + ) { + val generation = deviceGenerationMap[deviceId]?.plus(1) + ?: throw IllegalArgumentException("Device $deviceId was never added!") + deviceGenerationMap[deviceId] = generation + + `when`(iInputManager.getInputDevice(deviceId)) + .thenReturn(createInputDevice(deviceId, hasBattery, supportsUsi, generation)) val list = deviceGenerationMap.flatMap { listOf(it.key, it.value) } - devicesChangedListener.onInputDevicesChanged(list.toIntArray()) + if (::devicesChangedListener.isInitialized) { + devicesChangedListener.onInputDevicesChanged(list.toIntArray()) + } + } + + private fun addInputDevice( + deviceId: Int, + hasBattery: Boolean = true, + supportsUsi: Boolean = false + ) { + deviceGenerationMap[deviceId] = 0 + notifyDeviceChanged(deviceId, hasBattery, supportsUsi) } @After @@ -191,13 +239,6 @@ class BatteryControllerTests { InputManager.clearInstance() } - private fun createMockListener(): IInputDeviceBatteryListener { - val listener = mock(IInputDeviceBatteryListener::class.java) - val binder = mock(Binder::class.java) - `when`(listener.asBinder()).thenReturn(binder) - return listener - } - @Test fun testRegisterAndUnregisterBinderLifecycle() { val listener = createMockListener() @@ -303,19 +344,14 @@ class BatteryControllerTests { listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f) // If the battery presence for the InputDevice changes, the listener is notified. - `when`(iInputManager.getInputDevice(DEVICE_ID)) - .thenReturn(createInputDevice(DEVICE_ID, hasBattery = false)) - notifyDeviceChanged(DEVICE_ID) + notifyDeviceChanged(DEVICE_ID, hasBattery = false) testLooper.dispatchNext() - listener.verifyNotified(DEVICE_ID, isPresent = false, status = STATUS_UNKNOWN, - capacity = Float.NaN) + listener.verifyNotified(isInvalidBatteryState(DEVICE_ID)) // Since the battery is no longer present, the UEventListener should be removed. verify(uEventManager).removeListener(uEventListener.value) // If the battery becomes present again, the listener is notified. - `when`(iInputManager.getInputDevice(DEVICE_ID)) - .thenReturn(createInputDevice(DEVICE_ID, hasBattery = true)) - notifyDeviceChanged(DEVICE_ID) + notifyDeviceChanged(DEVICE_ID, hasBattery = true) testLooper.dispatchNext() listener.verifyNotified(DEVICE_ID, mode = times(2), status = STATUS_CHARGING, capacity = 0.78f) @@ -340,9 +376,17 @@ class BatteryControllerTests { // Move the time forward so that the polling period has elapsed. // The listener should be notified. - testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS - 1) + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS - 1) + assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle) testLooper.dispatchNext() listener.verifyNotified(DEVICE_ID, capacity = 0.80f) + + // Move the time forward so that another polling period has elapsed. + // The battery should still be polled, but there is no change so listeners are not notified. + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle) + testLooper.dispatchNext() + listener.verifyNotified(DEVICE_ID, mode = times(1), capacity = 0.80f) } @Test @@ -357,7 +401,8 @@ class BatteryControllerTests { // The battery state changed, but we should not be polling for battery changes when the // device is not interactive. `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80) - testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS) + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle) testLooper.dispatchAll() listener.verifyNotified(DEVICE_ID, mode = never(), capacity = 0.80f) @@ -368,7 +413,8 @@ class BatteryControllerTests { // Ensure that we continue to poll for battery changes. `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(90) - testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS) + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle) testLooper.dispatchNext() listener.verifyNotified(DEVICE_ID, capacity = 0.90f) } @@ -398,4 +444,44 @@ class BatteryControllerTests { matchesState(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f)) listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f) } + + @Test + fun testUsiDeviceIsMonitoredPersistently() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + + // Even though there is no listener added for this device, it is being monitored. + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + + // Add and remove a listener for the device. + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + batteryController.unregisterBatteryListener(USI_DEVICE_ID, listener, PID) + + // The device is still being monitored. + verify(uEventManager, never()).removeListener(uEventListener.value) + } + + @Test + fun testNoPollingWhenUsiDevicesAreMonitored() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + `when`(native.getBatteryDevicePath(SECOND_USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device2") + addInputDevice(SECOND_USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle) + + // Add a listener. + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle) + } } diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java index 94e67d16acab..3f55f1bb76a4 100644 --- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java +++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java @@ -16,16 +16,16 @@ package com.android.server.om; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static com.google.common.truth.Truth.assertThat; import android.content.om.OverlayIdentifier; import android.content.om.OverlayInfo; import androidx.test.runner.AndroidJUnit4; +import com.google.common.truth.Expect; + +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,6 +43,9 @@ public class OverlayManagerServiceImplRebootTests extends OverlayManagerServiceI private static final String OVERLAY2 = OVERLAY + "2"; private static final OverlayIdentifier IDENTIFIER2 = new OverlayIdentifier(OVERLAY2); + @Rule + public final Expect expect = Expect.create(); + @Test public void alwaysInitializeAllPackages() { final OverlayManagerServiceImpl impl = getImpl(); @@ -51,13 +54,11 @@ public class OverlayManagerServiceImplRebootTests extends OverlayManagerServiceI addPackage(target(otherTarget), USER); addPackage(overlay(OVERLAY, TARGET), USER); - final Set<PackageAndUser> allPackages = - Set.of(new PackageAndUser(TARGET, USER), - new PackageAndUser(otherTarget, USER), - new PackageAndUser(OVERLAY, USER)); + final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER)); - assertEquals(allPackages, impl.updateOverlaysForUser(USER)); - assertEquals(allPackages, impl.updateOverlaysForUser(USER)); + // The result should be the same for every time + assertThat(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages); + assertThat(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages); } @Test @@ -66,29 +67,31 @@ public class OverlayManagerServiceImplRebootTests extends OverlayManagerServiceI addPackage(target(TARGET), USER); addPackage(overlay(OVERLAY, TARGET), USER); - final Set<PackageAndUser> allPackages = - Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER)); + final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER)); configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_DISABLED, 0 /* priority */); - assertEquals(allPackages, impl.updateOverlaysForUser(USER)); + expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages); final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER); - assertNotNull(o1); - assertFalse(o1.isEnabled()); - assertFalse(o1.isMutable); + expect.that(o1).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o1.isEnabled()).isFalse(); + expect.that(o1.isMutable).isFalse(); configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_ENABLED, 0 /* priority */); - assertEquals(allPackages, impl.updateOverlaysForUser(USER)); + expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages); final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER, USER); - assertNotNull(o2); - assertTrue(o2.isEnabled()); - assertFalse(o2.isMutable); + expect.that(o2).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o2.isEnabled()).isTrue(); + expect.that(o2.isMutable).isFalse(); configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_DISABLED, 0 /* priority */); - assertEquals(allPackages, impl.updateOverlaysForUser(USER)); + expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages); final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER); - assertNotNull(o3); - assertFalse(o3.isEnabled()); - assertFalse(o3.isMutable); + expect.that(o3).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o3.isEnabled()).isFalse(); + expect.that(o3.isMutable).isFalse(); } @Test @@ -98,28 +101,30 @@ public class OverlayManagerServiceImplRebootTests extends OverlayManagerServiceI addPackage(overlay(OVERLAY, TARGET), USER); configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 0 /* priority */); - final Set<PackageAndUser> allPackages = - Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER)); + final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER)); - assertEquals(allPackages, impl.updateOverlaysForUser(USER)); + expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages); final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER); - assertNotNull(o1); - assertFalse(o1.isEnabled()); - assertTrue(o1.isMutable); + expect.that(o1).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o1.isEnabled()).isFalse(); + expect.that(o1.isMutable).isTrue(); configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_ENABLED, 0 /* priority */); - assertEquals(allPackages, impl.updateOverlaysForUser(USER)); + expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages); final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER, USER); - assertNotNull(o2); - assertFalse(o2.isEnabled()); - assertTrue(o2.isMutable); + expect.that(o2).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o2.isEnabled()).isFalse(); + expect.that(o2.isMutable).isTrue(); configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 0 /* priority */); - assertEquals(allPackages, impl.updateOverlaysForUser(USER)); + expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages); final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER); - assertNotNull(o3); - assertFalse(o3.isEnabled()); - assertTrue(o3.isMutable); + expect.that(o3).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o3.isEnabled()).isFalse(); + expect.that(o3.isMutable).isTrue(); } @Test @@ -128,17 +133,17 @@ public class OverlayManagerServiceImplRebootTests extends OverlayManagerServiceI addPackage(target(TARGET), USER); addPackage(overlay(OVERLAY, TARGET), USER); - final Set<PackageAndUser> allPackages = - Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER)); + final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER)); final Consumer<ConfigState> setOverlay = (state -> { configureSystemOverlay(OVERLAY, state, 0 /* priority */); - assertEquals(allPackages, impl.updateOverlaysForUser(USER)); + expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages); final OverlayInfo o = impl.getOverlayInfo(IDENTIFIER, USER); - assertNotNull(o); - assertEquals(o.isEnabled(), state == ConfigState.IMMUTABLE_ENABLED + expect.that(o).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o.isEnabled()).isEqualTo(state == ConfigState.IMMUTABLE_ENABLED || state == ConfigState.MUTABLE_ENABLED); - assertEquals(o.isMutable, state == ConfigState.MUTABLE_DISABLED + expect.that(o.isMutable).isEqualTo(state == ConfigState.MUTABLE_DISABLED || state == ConfigState.MUTABLE_ENABLED); }); @@ -180,20 +185,20 @@ public class OverlayManagerServiceImplRebootTests extends OverlayManagerServiceI configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 0 /* priority */); configureSystemOverlay(OVERLAY2, ConfigState.MUTABLE_DISABLED, 1 /* priority */); - final Set<PackageAndUser> allPackages = - Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER), - new PackageAndUser(OVERLAY2, USER)); + final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER)); - assertEquals(allPackages, impl.updateOverlaysForUser(USER)); + expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages); final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER); - assertNotNull(o1); - assertEquals(0, o1.priority); - assertFalse(o1.isEnabled()); + expect.that(o1).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o1.priority).isEqualTo(0); + expect.that(o1.isEnabled()).isFalse(); final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER2, USER); - assertNotNull(o2); - assertEquals(1, o2.priority); - assertFalse(o2.isEnabled()); + expect.that(o2).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o2.priority).isEqualTo(1); + expect.that(o2.isEnabled()).isFalse(); // Overlay priority changing between reboots should not affect enable state of mutable // overlays. @@ -202,16 +207,18 @@ public class OverlayManagerServiceImplRebootTests extends OverlayManagerServiceI // Reorder the overlays configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 1 /* priority */); configureSystemOverlay(OVERLAY2, ConfigState.MUTABLE_DISABLED, 0 /* priority */); - assertEquals(allPackages, impl.updateOverlaysForUser(USER)); + expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages); final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER); - assertNotNull(o3); - assertEquals(1, o3.priority); - assertTrue(o3.isEnabled()); + expect.that(o3).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o3.priority).isEqualTo(1); + expect.that(o3.isEnabled()).isTrue(); final OverlayInfo o4 = impl.getOverlayInfo(IDENTIFIER2, USER); - assertNotNull(o4); - assertEquals(0, o4.priority); - assertFalse(o4.isEnabled()); + expect.that(o4).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o4.priority).isEqualTo(0); + expect.that(o4.isEnabled()).isFalse(); } @Test @@ -223,33 +230,35 @@ public class OverlayManagerServiceImplRebootTests extends OverlayManagerServiceI configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_ENABLED, 0 /* priority */); configureSystemOverlay(OVERLAY2, ConfigState.IMMUTABLE_ENABLED, 1 /* priority */); - final Set<PackageAndUser> allPackages = - Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER), - new PackageAndUser(OVERLAY2, USER)); + final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER)); - assertEquals(allPackages, impl.updateOverlaysForUser(USER)); + expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages); final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER); - assertNotNull(o1); - assertEquals(0, o1.priority); - assertTrue(o1.isEnabled()); + expect.that(o1).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o1.priority).isEqualTo(0); + expect.that(o1.isEnabled()).isTrue(); final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER2, USER); - assertNotNull(o2); - assertEquals(1, o2.priority); - assertTrue(o2.isEnabled()); + expect.that(o2).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o2.priority).isEqualTo(1); + expect.that(o2.isEnabled()).isTrue(); // Reorder the overlays configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_ENABLED, 1 /* priority */); configureSystemOverlay(OVERLAY2, ConfigState.IMMUTABLE_ENABLED, 0 /* priority */); - assertEquals(allPackages, impl.updateOverlaysForUser(USER)); + expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages); final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER); - assertNotNull(o3); - assertEquals(1, o3.priority); - assertTrue(o3.isEnabled()); + expect.that(o3).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o3.priority).isEqualTo(1); + expect.that(o3.isEnabled()).isTrue(); final OverlayInfo o4 = impl.getOverlayInfo(IDENTIFIER2, USER); - assertNotNull(o4); - assertEquals(0, o4.priority); - assertTrue(o4.isEnabled()); + expect.that(o4).isNotNull(); + assertThat(expect.hasFailures()).isFalse(); + expect.that(o4.priority).isEqualTo(0); + expect.that(o4.isEnabled()).isTrue(); } } diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java index 96707fde8edb..00aa52012e59 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -176,6 +176,13 @@ public class UserManagerServiceTest { } @Test + public void testHasUserRestriction_NonExistentUserReturnsFalse() { + int nonExistentUserId = UserHandle.USER_NULL; + assertThat(mUserManagerService.hasUserRestriction(DISALLOW_USER_SWITCH, nonExistentUserId)) + .isFalse(); + } + + @Test public void testSetUserRestrictionWithIncorrectID() throws Exception { int incorrectId = 1; while (mUserManagerService.userExists(incorrectId)) { diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java index d5893c8d0b9f..77d542a2e43d 100644 --- a/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java +++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java @@ -52,7 +52,6 @@ public class DexoptOptionsTests { assertFalse(opt.isBootComplete()); assertFalse(opt.isCheckForProfileUpdates()); assertFalse(opt.isDexoptOnlySecondaryDex()); - assertFalse(opt.isDexoptOnlySharedDex()); assertFalse(opt.isDowngrade()); assertFalse(opt.isForce()); assertFalse(opt.isDexoptIdleBackgroundJob()); @@ -67,7 +66,6 @@ public class DexoptOptionsTests { DexoptOptions.DEXOPT_BOOT_COMPLETE | DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX | - DexoptOptions.DEXOPT_ONLY_SHARED_DEX | DexoptOptions.DEXOPT_DOWNGRADE | DexoptOptions.DEXOPT_AS_SHARED_LIBRARY | DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB | @@ -81,7 +79,6 @@ public class DexoptOptionsTests { assertTrue(opt.isBootComplete()); assertTrue(opt.isCheckForProfileUpdates()); assertTrue(opt.isDexoptOnlySecondaryDex()); - assertTrue(opt.isDexoptOnlySharedDex()); assertTrue(opt.isDowngrade()); assertTrue(opt.isForce()); assertTrue(opt.isDexoptAsSharedLibrary()); @@ -113,7 +110,6 @@ public class DexoptOptionsTests { assertTrue(opt.isBootComplete()); assertTrue(opt.isCheckForProfileUpdates()); assertFalse(opt.isDexoptOnlySecondaryDex()); - assertFalse(opt.isDexoptOnlySharedDex()); assertFalse(opt.isDowngrade()); assertTrue(opt.isForce()); assertFalse(opt.isDexoptAsSharedLibrary()); @@ -131,7 +127,6 @@ public class DexoptOptionsTests { assertTrue(opt.isBootComplete()); assertFalse(opt.isCheckForProfileUpdates()); assertFalse(opt.isDexoptOnlySecondaryDex()); - assertFalse(opt.isDexoptOnlySharedDex()); assertFalse(opt.isDowngrade()); assertTrue(opt.isForce()); assertFalse(opt.isDexoptAsSharedLibrary()); diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java index fe4db3a758e3..db2630e2683c 100644 --- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java @@ -87,7 +87,6 @@ import com.android.internal.app.IBatteryStats; import com.android.internal.util.test.FakeSettingsProvider; import com.android.server.LocalServices; import com.android.server.SystemService; -import com.android.server.compat.PlatformCompat; import com.android.server.lights.LightsManager; import com.android.server.policy.WindowManagerPolicy; import com.android.server.power.PowerManagerService.BatteryReceiver; @@ -147,7 +146,6 @@ public class PowerManagerServiceTest { @Mock private SystemPropertiesWrapper mSystemPropertiesMock; @Mock private AppOpsManager mAppOpsManagerMock; @Mock private LowPowerStandbyController mLowPowerStandbyControllerMock; - @Mock private PlatformCompat mPlatformCompat; @Mock private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock; @@ -321,11 +319,6 @@ public class PowerManagerServiceTest { AppOpsManager createAppOpsManager(Context context) { return mAppOpsManagerMock; } - - @Override - PlatformCompat createPlatformCompat(Context context) { - return mPlatformCompat; - } }); return mService; } @@ -505,9 +498,6 @@ public class PowerManagerServiceTest { String packageName = "pkg.name"; when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, Binder.getCallingUid(), packageName)).thenReturn(MODE_ALLOWED); - when(mPlatformCompat.isChangeEnabledByPackageName( - eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(), - anyInt())).thenReturn(true); when(mContextSpy.checkCallingOrSelfPermission( android.Manifest.permission.TURN_SCREEN_ON)).thenReturn( PackageManager.PERMISSION_GRANTED); @@ -532,23 +522,6 @@ public class PowerManagerServiceTest { null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */); - - // Verify that on older platforms only the appOp is necessary and the permission isn't - // checked - when(mPlatformCompat.isChangeEnabledByPackageName( - eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(), - anyInt())).thenReturn(false); - when(mContextSpy.checkCallingOrSelfPermission( - android.Manifest.permission.TURN_SCREEN_ON)).thenReturn( - PackageManager.PERMISSION_DENIED); - forceSleep(); - assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP); - - flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; - mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, - null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); - assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); - mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */); } @Test @@ -568,7 +541,7 @@ public class PowerManagerServiceTest { int flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); - if (PowerProperties.permissionless_turn_screen_on().orElse(true)) { + if (PowerProperties.permissionless_turn_screen_on().orElse(false)) { assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); } else { assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP); @@ -577,9 +550,6 @@ public class PowerManagerServiceTest { when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, Binder.getCallingUid(), packageName)).thenReturn(MODE_ALLOWED); - when(mPlatformCompat.isChangeEnabledByPackageName( - eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(), - anyInt())).thenReturn(true); when(mContextSpy.checkCallingOrSelfPermission( android.Manifest.permission.TURN_SCREEN_ON)).thenReturn( PackageManager.PERMISSION_DENIED); @@ -589,7 +559,7 @@ public class PowerManagerServiceTest { flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); - if (PowerProperties.permissionless_turn_screen_on().orElse(true)) { + if (PowerProperties.permissionless_turn_screen_on().orElse(false)) { assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); } else { assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP); diff --git a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java index 235849c1cd8b..c484f457faea 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java @@ -53,7 +53,8 @@ final class FakeVibratorControllerProvider { private boolean mIsAvailable = true; private boolean mIsInfoLoadSuccessful = true; - private long mLatency; + private long mOnLatency; + private long mOffLatency; private int mOffCount; private int mCapabilities; @@ -97,7 +98,7 @@ final class FakeVibratorControllerProvider { public long on(long milliseconds, long vibrationId) { recordEffectSegment(vibrationId, new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, /* frequencyHz= */ 0, (int) milliseconds)); - applyLatency(); + applyLatency(mOnLatency); scheduleListener(milliseconds, vibrationId); return milliseconds; } @@ -105,12 +106,13 @@ final class FakeVibratorControllerProvider { @Override public void off() { mOffCount++; + applyLatency(mOffLatency); } @Override public void setAmplitude(float amplitude) { mAmplitudes.add(amplitude); - applyLatency(); + applyLatency(mOnLatency); } @Override @@ -121,7 +123,7 @@ final class FakeVibratorControllerProvider { } recordEffectSegment(vibrationId, new PrebakedSegment((int) effect, false, (int) strength)); - applyLatency(); + applyLatency(mOnLatency); scheduleListener(EFFECT_DURATION, vibrationId); return EFFECT_DURATION; } @@ -141,7 +143,7 @@ final class FakeVibratorControllerProvider { duration += EFFECT_DURATION + primitive.getDelay(); recordEffectSegment(vibrationId, primitive); } - applyLatency(); + applyLatency(mOnLatency); scheduleListener(duration, vibrationId); return duration; } @@ -154,7 +156,7 @@ final class FakeVibratorControllerProvider { recordEffectSegment(vibrationId, primitive); } recordBraking(vibrationId, braking); - applyLatency(); + applyLatency(mOnLatency); scheduleListener(duration, vibrationId); return duration; } @@ -193,10 +195,10 @@ final class FakeVibratorControllerProvider { return mIsInfoLoadSuccessful; } - private void applyLatency() { + private void applyLatency(long latencyMillis) { try { - if (mLatency > 0) { - Thread.sleep(mLatency); + if (latencyMillis > 0) { + Thread.sleep(latencyMillis); } } catch (InterruptedException e) { } @@ -240,10 +242,15 @@ final class FakeVibratorControllerProvider { /** * Sets the latency this controller should fake for turning the vibrator hardware on or setting - * it's vibration amplitude. + * the vibration amplitude. */ - public void setLatency(long millis) { - mLatency = millis; + public void setOnLatency(long millis) { + mOnLatency = millis; + } + + /** Sets the latency this controller should fake for turning the vibrator off. */ + public void setOffLatency(long millis) { + mOffLatency = millis; } /** Set the capabilities of the fake vibrator hardware. */ diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java index a15e4b0c74a0..fc830a9f61ad 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java @@ -1159,7 +1159,7 @@ public class VibrationThreadTest { // 25% of the first waveform step will be spent on the native on() call. // 25% of each waveform step will be spent on the native setAmplitude() call.. - mVibratorProviders.get(VIBRATOR_ID).setLatency(stepDuration / 4); + mVibratorProviders.get(VIBRATOR_ID).setOnLatency(stepDuration / 4); mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); int stepCount = totalDuration / stepDuration; @@ -1190,7 +1190,7 @@ public class VibrationThreadTest { fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); long latency = 5_000; // 5s - fakeVibrator.setLatency(latency); + fakeVibrator.setOnLatency(latency); long vibrationId = 1; VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); @@ -1204,8 +1204,7 @@ public class VibrationThreadTest { // fail at waitForCompletion(cancellingThread). Thread cancellingThread = new Thread( () -> conductor.notifyCancelled( - new Vibration.EndInfo( - Vibration.Status.CANCELLED_BY_USER), + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER), /* immediate= */ false)); cancellingThread.start(); diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java index c46fecd1a55e..c83afb74ccba 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -826,13 +826,40 @@ public class VibratorManagerServiceTest { // The second vibration shouldn't have recorded that the vibrators were turned on. verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong()); // No segment played is the prebaked CLICK from the second vibration. - assertFalse( - mVibratorProviders.get(1).getAllEffectSegments().stream() - .anyMatch(segment -> segment instanceof PrebakedSegment)); + assertFalse(mVibratorProviders.get(1).getAllEffectSegments().stream() + .anyMatch(PrebakedSegment.class::isInstance)); cancelVibrate(service); // Clean up repeating effect. } @Test + public void vibrate_withOngoingRepeatingVibrationBeingCancelled_playsAfterPreviousIsCancelled() + throws Exception { + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setOffLatency(50); // Add latency so cancellation is slow. + fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); + VibratorManagerService service = createSystemReadyService(); + + VibrationEffect repeatingEffect = VibrationEffect.createWaveform( + new long[]{10, 10_000}, new int[]{255, 0}, 1); + vibrate(service, repeatingEffect, ALARM_ATTRS); + + // VibrationThread will start this vibration async, wait until the off waveform step. + assertTrue(waitUntil(s -> fakeVibrator.getOffCount() > 0, service, TEST_TIMEOUT_MILLIS)); + + // Cancel vibration right before requesting a new one. + // This should trigger slow IVibrator.off before setting the vibration status to cancelled. + cancelVibrate(service); + vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), + ALARM_ATTRS); + + // Check that second vibration was played. + assertTrue(fakeVibrator.getAllEffectSegments().stream() + .anyMatch(PrebakedSegment.class::isInstance)); + } + + @Test public void vibrate_withNewRepeatingVibration_cancelsOngoingEffect() throws Exception { mockVibrators(1); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); @@ -880,10 +907,8 @@ public class VibratorManagerServiceTest { // The second vibration shouldn't have recorded that the vibrators were turned on. verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong()); // The second vibration shouldn't have played any prebaked segment. - assertFalse( - mVibratorProviders.get(1).getAllEffectSegments().stream() - .anyMatch(segment -> segment instanceof PrebakedSegment)); - + assertFalse(mVibratorProviders.get(1).getAllEffectSegments().stream() + .anyMatch(PrebakedSegment.class::isInstance)); cancelVibrate(service); // Clean up long effect. } diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java index 91c2fe0eb262..8e81e2d8997c 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java @@ -1371,6 +1371,39 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { verify(mInjector).startDreamWhenDockedIfAppropriate(mContext); } + @Test + public void dreamWhenDocked_ambientModeSuppressed_suppressionEnabled() { + mUiManagerService.setStartDreamImmediatelyOnDock(true); + mUiManagerService.setDreamsDisabledByAmbientModeSuppression(true); + + when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(true); + triggerDockIntent(); + verifyAndSendResultBroadcast(); + verify(mInjector, never()).startDreamWhenDockedIfAppropriate(mContext); + } + + @Test + public void dreamWhenDocked_ambientModeSuppressed_suppressionDisabled() { + mUiManagerService.setStartDreamImmediatelyOnDock(true); + mUiManagerService.setDreamsDisabledByAmbientModeSuppression(false); + + when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(true); + triggerDockIntent(); + verifyAndSendResultBroadcast(); + verify(mInjector).startDreamWhenDockedIfAppropriate(mContext); + } + + @Test + public void dreamWhenDocked_ambientModeNotSuppressed_suppressionEnabled() { + mUiManagerService.setStartDreamImmediatelyOnDock(true); + mUiManagerService.setDreamsDisabledByAmbientModeSuppression(true); + + when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(false); + triggerDockIntent(); + verifyAndSendResultBroadcast(); + verify(mInjector).startDreamWhenDockedIfAppropriate(mContext); + } + private void triggerDockIntent() { final Intent dockedIntent = new Intent(Intent.ACTION_DOCK_EVENT) diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java index 1e945776cf40..248a3fc8b3b1 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java @@ -15,6 +15,7 @@ */ package com.android.server.notification; +import static android.content.pm.PackageManager.MATCH_ANY_USER; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING; @@ -30,9 +31,11 @@ import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.intThat; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -49,6 +52,7 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.content.pm.VersionedPackage; +import android.content.res.Resources; import android.os.Bundle; import android.os.UserHandle; import android.service.notification.NotificationListenerFilter; @@ -69,6 +73,7 @@ import com.google.common.collect.ImmutableList; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.internal.util.reflection.FieldSetter; @@ -77,6 +82,7 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.util.Arrays; import java.util.List; public class NotificationListenersTest extends UiServiceTestCase { @@ -85,6 +91,8 @@ public class NotificationListenersTest extends UiServiceTestCase { private PackageManager mPm; @Mock private IPackageManager miPm; + @Mock + private Resources mResources; @Mock NotificationManagerService mNm; @@ -96,7 +104,8 @@ public class NotificationListenersTest extends UiServiceTestCase { private ComponentName mCn1 = new ComponentName("pkg", "pkg.cmp"); private ComponentName mCn2 = new ComponentName("pkg2", "pkg2.cmp2"); - + private ComponentName mUninstalledComponent = new ComponentName("pkg3", + "pkg3.NotificationListenerService"); @Before public void setUp() throws Exception { @@ -111,7 +120,7 @@ public class NotificationListenersTest extends UiServiceTestCase { @Test public void testReadExtraTag() throws Exception { - String xml = "<" + TAG_REQUESTED_LISTENERS+ ">" + String xml = "<" + TAG_REQUESTED_LISTENERS + ">" + "<listener component=\"" + mCn1.flattenToString() + "\" user=\"0\">" + "<allowed types=\"7\" />" + "</listener>" @@ -131,11 +140,55 @@ public class NotificationListenersTest extends UiServiceTestCase { } @Test + public void loadDefaultsFromConfig_forHeadlessSystemUser_loadUninstalled() throws Exception { + // setup with headless system user mode + mListeners = spy(mNm.new NotificationListeners( + mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm, + /* isHeadlessSystemUserMode= */ true)); + mockDefaultListenerConfigForUninstalledComponent(mUninstalledComponent); + + mListeners.loadDefaultsFromConfig(); + + assertThat(mListeners.getDefaultComponents()).contains(mUninstalledComponent); + } + + @Test + public void loadDefaultsFromConfig_forNonHeadlessSystemUser_ignoreUninstalled() + throws Exception { + // setup without headless system user mode + mListeners = spy(mNm.new NotificationListeners( + mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm, + /* isHeadlessSystemUserMode= */ false)); + mockDefaultListenerConfigForUninstalledComponent(mUninstalledComponent); + + mListeners.loadDefaultsFromConfig(); + + assertThat(mListeners.getDefaultComponents()).doesNotContain(mUninstalledComponent); + } + + private void mockDefaultListenerConfigForUninstalledComponent(ComponentName componentName) { + ArraySet<ComponentName> components = new ArraySet<>(Arrays.asList(componentName)); + when(mResources + .getString( + com.android.internal.R.string.config_defaultListenerAccessPackages)) + .thenReturn(componentName.getPackageName()); + when(mContext.getResources()).thenReturn(mResources); + doReturn(components).when(mListeners).queryPackageForServices( + eq(componentName.getPackageName()), + intThat(hasIntBitFlag(MATCH_ANY_USER)), + anyInt()); + } + + public static ArgumentMatcher<Integer> hasIntBitFlag(int flag) { + return arg -> arg != null && ((arg & flag) == flag); + } + + @Test public void testWriteExtraTag() throws Exception { NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>()); VersionedPackage a1 = new VersionedPackage("pkg1", 243); NotificationListenerFilter nlf2 = - new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[] {a1})); + new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[]{a1})); mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf); mListeners.setNotificationListenerFilter(Pair.create(mCn2, 10), nlf2); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java deleted file mode 100644 index d7650420788c..000000000000 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java +++ /dev/null @@ -1,551 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.server.notification; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertNull; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import android.app.ActivityManager; -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Person; -import android.app.RemoteInput; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Typeface; -import android.graphics.drawable.Icon; -import android.net.Uri; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.style.StyleSpan; -import android.util.Pair; -import android.widget.RemoteViews; - -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.server.UiServiceTestCase; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class NotificationTest extends UiServiceTestCase { - - @Mock - ActivityManager mAm; - - @Mock - Resources mResources; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testDoesNotStripsExtenders() { - Notification.Builder nb = new Notification.Builder(mContext, "channel"); - nb.extend(new Notification.CarExtender().setColor(Color.RED)); - nb.extend(new Notification.TvExtender().setChannelId("different channel")); - nb.extend(new Notification.WearableExtender().setDismissalId("dismiss")); - Notification before = nb.build(); - Notification after = Notification.Builder.maybeCloneStrippedForDelivery(before); - - assertTrue(before == after); - - assertEquals("different channel", new Notification.TvExtender(before).getChannelId()); - assertEquals(Color.RED, new Notification.CarExtender(before).getColor()); - assertEquals("dismiss", new Notification.WearableExtender(before).getDismissalId()); - } - - @Test - public void testStyleChangeVisiblyDifferent_noStyles() { - Notification.Builder n1 = new Notification.Builder(mContext, "test"); - Notification.Builder n2 = new Notification.Builder(mContext, "test"); - - assertFalse(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testStyleChangeVisiblyDifferent_noStyleToStyle() { - Notification.Builder n1 = new Notification.Builder(mContext, "test"); - Notification.Builder n2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle()); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testStyleChangeVisiblyDifferent_styleToNoStyle() { - Notification.Builder n2 = new Notification.Builder(mContext, "test"); - Notification.Builder n1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle()); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testStyleChangeVisiblyDifferent_changeStyle() { - Notification.Builder n1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.InboxStyle()); - Notification.Builder n2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle()); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testInboxTextChange() { - Notification.Builder nInbox1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.InboxStyle().addLine("a").addLine("b")); - Notification.Builder nInbox2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.InboxStyle().addLine("b").addLine("c")); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nInbox1, nInbox2)); - } - - @Test - public void testBigTextTextChange() { - Notification.Builder nBigText1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle().bigText("something")); - Notification.Builder nBigText2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle().bigText("else")); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigText1, nBigText2)); - } - - @Test - public void testBigPictureChange() { - Bitmap bitA = mock(Bitmap.class); - when(bitA.getGenerationId()).thenReturn(100); - Bitmap bitB = mock(Bitmap.class); - when(bitB.getGenerationId()).thenReturn(200); - - Notification.Builder nBigPic1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigPictureStyle().bigPicture(bitA)); - Notification.Builder nBigPic2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigPictureStyle().bigPicture(bitB)); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigPic1, nBigPic2)); - } - - @Test - public void testMessagingChange_text() { - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class))) - .addMessage(new Notification.MessagingStyle.Message( - "b", 100, mock(Person.class))) - ); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_data() { - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)) - .setData("text", mock(Uri.class)))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)))); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_sender() { - Person a = mock(Person.class); - when(a.getName()).thenReturn("A"); - Person b = mock(Person.class); - when(b.getName()).thenReturn("b"); - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_key() { - Person a = mock(Person.class); - when(a.getKey()).thenReturn("A"); - Person b = mock(Person.class); - when(b.getKey()).thenReturn("b"); - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_ignoreTimeChange() { - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 1000, mock(Person.class))) - ); - - assertFalse(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testRemoteViews_nullChange() { - Notification.Builder n1 = new Notification.Builder(mContext, "test") - .setContent(mock(RemoteViews.class)); - Notification.Builder n2 = new Notification.Builder(mContext, "test"); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test"); - n2 = new Notification.Builder(mContext, "test") - .setContent(mock(RemoteViews.class)); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test") - .setCustomBigContentView(mock(RemoteViews.class)); - n2 = new Notification.Builder(mContext, "test"); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test"); - n2 = new Notification.Builder(mContext, "test") - .setCustomBigContentView(mock(RemoteViews.class)); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test"); - n2 = new Notification.Builder(mContext, "test"); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_layoutChange() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(189); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_layoutSame() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(234); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_sequenceChange() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - when(a.getSequenceNumber()).thenReturn(1); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(234); - when(b.getSequenceNumber()).thenReturn(2); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_sequenceSame() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - when(a.getSequenceNumber()).thenReturn(1); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(234); - when(b.getSequenceNumber()).thenReturn(1); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testActionsDifferent_null() { - Notification n1 = new Notification.Builder(mContext, "test") - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentSame() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentText() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) - .build(); - - assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentSpannables() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, - new SpannableStringBuilder().append("test1", - new StyleSpan(Typeface.BOLD), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE), - intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "test1", intent).build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentNumber() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) - .build(); - - assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentIntent() { - PendingIntent intent1 = mock(PendingIntent.class); - PendingIntent intent2 = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent1).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent2).build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsIgnoresRemoteInputs() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(new RemoteInput.Builder("a") - .setChoices(new CharSequence[] {"i", "m"}) - .build()) - .build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(new RemoteInput.Builder("a") - .setChoices(new CharSequence[] {"t", "m"}) - .build()) - .build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testFreeformRemoteInputActionPair_noRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - Notification notification = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .build()) - .build(); - assertNull(notification.findRemoteInputActionPair(false)); - } - - @Test - public void testFreeformRemoteInputActionPair_hasRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - RemoteInput remoteInput = new RemoteInput.Builder("a").build(); - - Notification.Action actionWithRemoteInput = - new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(remoteInput) - .addRemoteInput(remoteInput) - .build(); - - Notification.Action actionWithoutRemoteInput = - new Notification.Action.Builder(icon, "TEXT 2", intent) - .build(); - - Notification notification = new Notification.Builder(mContext, "test") - .addAction(actionWithoutRemoteInput) - .addAction(actionWithRemoteInput) - .build(); - - Pair<RemoteInput, Notification.Action> remoteInputActionPair = - notification.findRemoteInputActionPair(false); - - assertNotNull(remoteInputActionPair); - assertEquals(remoteInput, remoteInputActionPair.first); - assertEquals(actionWithRemoteInput, remoteInputActionPair.second); - } - - @Test - public void testFreeformRemoteInputActionPair_requestFreeform_noFreeformRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - Notification notification = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput( - new RemoteInput.Builder("a") - .setAllowFreeFormInput(false).build()) - .build()) - .build(); - assertNull(notification.findRemoteInputActionPair(true)); - } - - @Test - public void testFreeformRemoteInputActionPair_requestFreeform_hasFreeformRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - RemoteInput remoteInput = - new RemoteInput.Builder("a").setAllowFreeFormInput(false).build(); - RemoteInput freeformRemoteInput = - new RemoteInput.Builder("b").setAllowFreeFormInput(true).build(); - - Notification.Action actionWithFreeformRemoteInput = - new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(remoteInput) - .addRemoteInput(freeformRemoteInput) - .build(); - - Notification.Action actionWithoutFreeformRemoteInput = - new Notification.Action.Builder(icon, "TEXT 2", intent) - .addRemoteInput(remoteInput) - .build(); - - Notification notification = new Notification.Builder(mContext, "test") - .addAction(actionWithoutFreeformRemoteInput) - .addAction(actionWithFreeformRemoteInput) - .build(); - - Pair<RemoteInput, Notification.Action> remoteInputActionPair = - notification.findRemoteInputActionPair(true); - - assertNotNull(remoteInputActionPair); - assertEquals(freeformRemoteInput, remoteInputActionPair.first); - assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second); - } -} - diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java index d5e336b1cf2f..eed32d7d815c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java @@ -40,14 +40,18 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; +import android.app.ActivityOptions; import android.app.WaitResult; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.os.Binder; import android.os.ConditionVariable; +import android.os.IBinder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; import android.view.Display; @@ -308,4 +312,40 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase { waitHandlerIdle(mAtm.mH); verify(mRootWindowContainer, timeout(TIMEOUT_MS)).startHomeOnEmptyDisplays("userUnlocked"); } + + /** Verifies that launch from recents sets the launch cookie on the activity. */ + @Test + public void testStartActivityFromRecents_withLaunchCookie() { + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + + IBinder launchCookie = new Binder("test_launch_cookie"); + ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchCookie(launchCookie); + SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(options.toBundle()); + + doNothing().when(mSupervisor.mService).moveTaskToFrontLocked(eq(null), eq(null), anyInt(), + anyInt(), any()); + + mSupervisor.startActivityFromRecents(-1, -1, activity.getRootTaskId(), safeOptions); + + assertThat(activity.mLaunchCookie).isEqualTo(launchCookie); + verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions)); + } + + /** Verifies that launch from recents doesn't set the launch cookie on the activity. */ + @Test + public void testStartActivityFromRecents_withoutLaunchCookie() { + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + + SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle( + ActivityOptions.makeBasic().toBundle()); + + doNothing().when(mSupervisor.mService).moveTaskToFrontLocked(eq(null), eq(null), anyInt(), + anyInt(), any()); + + mSupervisor.startActivityFromRecents(-1, -1, activity.getRootTaskId(), safeOptions); + + assertThat(activity.mLaunchCookie).isNull(); + verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions)); + } } diff --git a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java index 2ae328b0d8e9..394d6e774aa1 100644 --- a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java +++ b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java @@ -19,6 +19,7 @@ package com.android.server.usb; import android.annotation.NonNull; import android.content.Context; import android.hardware.usb.UsbConfiguration; +import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbEndpoint; @@ -76,10 +77,10 @@ public final class UsbDirectMidiDevice implements Closeable { // event schedulers for each input port of the physical device private MidiEventScheduler[] mEventSchedulers; - // Arbitrary number for timeout to not continue sending to - // an inactive device. This number tries to balances the number - // of cycles and not being permanently stuck. - private static final int BULK_TRANSFER_TIMEOUT_MILLISECONDS = 10; + // Timeout for sending a packet to a device. + // If bulkTransfer times out, retry sending the packet up to 20 times. + private static final int BULK_TRANSFER_TIMEOUT_MILLISECONDS = 50; + private static final int BULK_TRANSFER_NUMBER_OF_RETRIES = 20; // Arbitrary number for timeout when closing a thread private static final int THREAD_JOIN_TIMEOUT_MILLISECONDS = 200; @@ -386,10 +387,15 @@ public final class UsbDirectMidiDevice implements Closeable { break; } final UsbRequest response = connectionFinal.requestWait(); - if (response != request) { - Log.w(TAG, "Unexpected response"); + if (response == null) { + Log.w(TAG, "Response is null"); break; } + if (request != response) { + Log.w(TAG, "Skipping response"); + continue; + } + int bytesRead = byteBuffer.position(); if (bytesRead > 0) { @@ -513,9 +519,47 @@ public final class UsbDirectMidiDevice implements Closeable { convertedArray.length); } - connectionFinal.bulkTransfer(endpointFinal, convertedArray, - convertedArray.length, - BULK_TRANSFER_TIMEOUT_MILLISECONDS); + boolean isInterrupted = false; + // Split the packet into multiple if they are greater than the + // endpoint's max packet size. + for (int curPacketStart = 0; + curPacketStart < convertedArray.length && + isInterrupted == false; + curPacketStart += endpointFinal.getMaxPacketSize()) { + int transferResult = -1; + int retryCount = 0; + int curPacketSize = Math.min(endpointFinal.getMaxPacketSize(), + convertedArray.length - curPacketStart); + + // Keep trying to send the packet until the result is + // successful or until the retry limit is reached. + while (transferResult < 0 && retryCount <= + BULK_TRANSFER_NUMBER_OF_RETRIES) { + transferResult = connectionFinal.bulkTransfer( + endpointFinal, + convertedArray, + curPacketStart, + curPacketSize, + BULK_TRANSFER_TIMEOUT_MILLISECONDS); + retryCount++; + + if (Thread.currentThread().interrupted()) { + Log.w(TAG, "output thread interrupted after send"); + isInterrupted = true; + break; + } + if (transferResult < 0) { + Log.d(TAG, "retrying packet. retryCount = " + + retryCount + " result = " + transferResult); + if (retryCount > BULK_TRANSFER_NUMBER_OF_RETRIES) { + Log.w(TAG, "Skipping packet because timeout"); + } + } + } + } + if (isInterrupted == true) { + break; + } eventSchedulerFinal.addEventToPool(event); } } catch (NullPointerException e) { diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index f43f0a53650b..d314a6579b58 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -8658,11 +8658,12 @@ public class CarrierConfigManager { /** * Boolean indicating if the VoNR setting is visible in the Call Settings menu. - * If true, the VoNR setting menu will be visible. If false, the menu will be gone. + * If this flag is set and VoNR is enabled for this carrier (see {@link #KEY_VONR_ENABLED_BOOL}) + * the VoNR setting menu will be visible. If {@link #KEY_VONR_ENABLED_BOOL} or + * this setting is false, the menu will be gone. * - * Disabled by default. + * Enabled by default. * - * @hide */ public static final String KEY_VONR_SETTING_VISIBILITY_BOOL = "vonr_setting_visibility_bool"; @@ -8672,7 +8673,6 @@ public class CarrierConfigManager { * * Disabled by default. * - * @hide */ public static final String KEY_VONR_ENABLED_BOOL = "vonr_enabled_bool"; @@ -8715,6 +8715,8 @@ public class CarrierConfigManager { * premium capabilities should be blocked when * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)} * returns a failure due to user action or timeout. + * The maximum number of network boost notifications to show the user are defined in + * {@link #KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY}. * * The default value is 30 minutes. * @@ -8726,6 +8728,22 @@ public class CarrierConfigManager { "premium_capability_notification_backoff_hysteresis_time_millis_long"; /** + * The maximum number of times that we display the notification for a network boost via premium + * capabilities when {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)} + * returns a failure due to user action or timeout. + * + * An int array with 2 values: {max_notifications_per_day, max_notifications_per_month}. + * + * The default value is {2, 10}, meaning we display a maximum of 2 network boost notifications + * per day and 10 notifications per month. + * + * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED + * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT + */ + public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY = + "premium_capability_maximum_notification_count_int_array"; + + /** * The amount of time in milliseconds that the purchase request should be throttled when * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)} * returns a failure due to the carrier. @@ -8752,6 +8770,20 @@ public class CarrierConfigManager { "premium_capability_purchase_url_string"; /** + * Whether to allow premium capabilities to be purchased when the device is connected to LTE. + * If this is {@code true}, applications can call + * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)} + * when connected to {@link TelephonyManager#NETWORK_TYPE_LTE} to purchase and use + * premium capabilities. + * If this is {@code false}, applications can only purchase and use premium capabilities when + * conencted to {@link TelephonyManager#NETWORK_TYPE_NR}. + * + * This is {@code false} by default. + */ + public static final String KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL = + "premium_capability_supported_on_lte_bool"; + + /** * IWLAN handover rules that determine whether handover is allowed or disallowed between * cellular and IWLAN. * @@ -9432,15 +9464,18 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_UNTHROTTLE_DATA_RETRY_WHEN_TAC_CHANGES_BOOL, false); sDefaults.putBoolean(KEY_VONR_SETTING_VISIBILITY_BOOL, true); sDefaults.putBoolean(KEY_VONR_ENABLED_BOOL, false); - sDefaults.putIntArray(KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY, new int[]{}); + sDefaults.putIntArray(KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY, new int[] {}); sDefaults.putLong(KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG, TimeUnit.MINUTES.toMillis(30)); sDefaults.putLong(KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG, TimeUnit.MINUTES.toMillis(30)); + sDefaults.putIntArray(KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY, + new int[] {2, 10}); sDefaults.putLong( KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG, TimeUnit.MINUTES.toMillis(30)); sDefaults.putString(KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING, null); + sDefaults.putBoolean(KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL, false); sDefaults.putStringArray(KEY_IWLAN_HANDOVER_POLICY_STRING_ARRAY, new String[]{ "source=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, " + "target=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, type=allowed"}); diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index f3d48a849b0f..97a464cb76da 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -54,6 +54,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.ConnectivityManager; +import android.net.NetworkCapabilities; import android.net.Uri; import android.os.AsyncTask; import android.os.Binder; @@ -17115,11 +17116,12 @@ public class TelephonyManager { } /** - * A premium capability boosting the network to allow real-time interactive traffic. - * Corresponds to NetworkCapabilities#NET_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC. + * A premium capability that boosts the network to allow for real-time interactive traffic + * by prioritizing low latency communication. + * Corresponds to {@link NetworkCapabilities#NET_CAPABILITY_PRIORITIZE_LATENCY}. */ - // TODO(b/245748544): add @link once NET_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC is defined. - public static final int PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC = 1; + public static final int PREMIUM_CAPABILITY_PRIORITIZE_LATENCY = + NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY; /** * Purchasable premium capabilities. @@ -17127,7 +17129,7 @@ public class TelephonyManager { */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = { "PREMIUM_CAPABILITY_" }, value = { - PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC}) + PREMIUM_CAPABILITY_PRIORITIZE_LATENCY}) public @interface PremiumCapability {} /** @@ -17139,8 +17141,8 @@ public class TelephonyManager { */ public static String convertPremiumCapabilityToString(@PremiumCapability int capability) { switch (capability) { - case PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC: - return "REALTIME_INTERACTIVE_TRAFFIC"; + case PREMIUM_CAPABILITY_PRIORITIZE_LATENCY: + return "PRIORITIZE_LATENCY"; default: return "UNKNOWN (" + capability + ")"; } @@ -17178,11 +17180,18 @@ public class TelephonyManager { public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS = 1; /** - * Purchase premium capability failed because the request is throttled for the amount of time + * Purchase premium capability failed because the request is throttled. + * If purchasing premium capabilities is throttled, it will be for the amount of time * specified by {@link CarrierConfigManager - * #KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG} - * or {@link CarrierConfigManager * #KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}. + * If displaying the network boost notification is throttled, it will be for the amount of time + * specified by {@link CarrierConfigManager + * #KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_INT_ARRAY}. + * If a foreground application requests premium capabilities, the network boost notification + * will be displayed to the user regardless of the throttled status. + * We will show the network boost notification to the user up to the daily and monthly maximum + * number of times specified by {@link CarrierConfigManager + * #KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY}. * Subsequent attempts will return the same error until the request is no longer throttled * or throttling conditions change. */ @@ -17202,10 +17211,14 @@ public class TelephonyManager { public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS = 4; /** - * Purchase premium capability failed because the user disabled the feature. - * Subsequent attempts will return the same error until the user re-enables the feature. + * Purchase premium capability failed because a foreground application requested the same + * capability. The notification for the current application will be dismissed and a new + * notification will be displayed to the user for the foreground application. + * Subsequent attempts will return + * {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS} until the foreground + * application's request is completed. */ - public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED = 5; + public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5; /** * Purchase premium capability failed because the user canceled the operation. @@ -17252,7 +17265,8 @@ public class TelephonyManager { public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED = 10; /** - * Purchase premium capability failed because the telephony service is down or unavailable. + * Purchase premium capability failed because the telephony service is unavailable + * or there was an error in the phone process. * Subsequent attempts will return the same error until request conditions are satisfied. */ public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11; @@ -17274,6 +17288,14 @@ public class TelephonyManager { public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED = 13; /** + * Purchase premium capability failed because the request was not made on the default data + * subscription, indicated by {@link SubscriptionManager#getDefaultDataSubscriptionId()}. + * Subsequent attempts will return the same error until the request is made on the default + * data subscription. + */ + public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA = 14; + + /** * Results of the purchase premium capability request. * @hide */ @@ -17283,14 +17305,15 @@ public class TelephonyManager { PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED, PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED, PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS, - PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED, + PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN, PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED, PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED, PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_ERROR, PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT, PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED, PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE, - PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED}) + PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED, + PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA}) public @interface PurchasePremiumCapabilityResult {} /** @@ -17311,8 +17334,8 @@ public class TelephonyManager { return "ALREADY_PURCHASED"; case PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS: return "ALREADY_IN_PROGRESS"; - case PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED: - return "USER_DISABLED"; + case PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN: + return "OVERRIDDEN"; case PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED: return "USER_CANCELED"; case PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED: @@ -17329,6 +17352,8 @@ public class TelephonyManager { return "NETWORK_NOT_AVAILABLE"; case PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED: return "NETWORK_CONGESTED"; + case PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA: + return "NOT_DEFAULT_DATA"; default: return "UNKNOWN (" + result + ")"; } @@ -17346,7 +17371,7 @@ public class TelephonyManager { * @param callback The result of the purchase request. * One of {@link PurchasePremiumCapabilityResult}. * @throws SecurityException if the caller does not hold permission READ_BASIC_PHONE_STATE. - * @see #isPremiumCapabilityAvailableForPurchase(int) to check whether the capability is valid + * @see #isPremiumCapabilityAvailableForPurchase(int) to check whether the capability is valid. */ @RequiresPermission(android.Manifest.permission.READ_BASIC_PHONE_STATE) public void purchasePremiumCapability(@PremiumCapability int capability, diff --git a/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml b/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml index 5a135c978343..7fe4bae2a3fe 100644 --- a/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml +++ b/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml @@ -16,7 +16,7 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.sample.rollbackapp" > - <uses-permission android:name="android.permission.TEST_MANAGE_ROLLBACKS" /> + <uses-permission android:name="android.permission.MANAGE_ROLLBACKS" /> <application android:label="@string/title_activity_main"> <activity @@ -28,4 +28,4 @@ </intent-filter> </activity> </application> -</manifest>
\ No newline at end of file +</manifest> diff --git a/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java b/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java index 916551a8ce6d..79a2f1f5f4de 100644 --- a/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java +++ b/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java @@ -75,6 +75,7 @@ public class MainActivity extends Activity { String rollbackStatus = "FAILED"; if (rollbackStatusCode == RollbackManager.STATUS_SUCCESS) { rollbackStatus = "SUCCESS"; + mTriggerRollbackButton.setClickable(false); } makeToast("Status for rollback ID " + rollbackId + " is " + rollbackStatus); }}, new IntentFilter(ACTION_NAME), Context.RECEIVER_NOT_EXPORTED); diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt index 68a450d956a8..1b0f03564c3b 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt +++ b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt @@ -26,6 +26,7 @@ import com.android.tools.lint.detector.api.Scope import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.getUMethod +import com.google.android.lint.aidl.hasPermissionMethodAnnotation import com.intellij.psi.PsiType import org.jetbrains.uast.UAnnotation import org.jetbrains.uast.UBlockExpression @@ -149,11 +150,6 @@ class PermissionMethodDetector : Detector(), SourceCodeScanner { enabledByDefault = false ) - private fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations - .any { - it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD) - } - private fun isPermissionMethodReturnType(method: UMethod): Boolean = listOf(PsiType.VOID, PsiType.INT, PsiType.BOOLEAN).contains(method.returnType) diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt index 510611161ea8..d120e1d41c99 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt +++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt @@ -18,37 +18,54 @@ package com.google.android.lint.aidl import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Location -import com.intellij.psi.PsiVariable +import com.android.tools.lint.detector.api.getUMethod +import org.jetbrains.kotlin.psi.psiUtil.parameterIndex import org.jetbrains.uast.UCallExpression -import org.jetbrains.uast.ULiteralExpression -import org.jetbrains.uast.UQualifiedReferenceExpression -import org.jetbrains.uast.USimpleNameReferenceExpression -import org.jetbrains.uast.asRecursiveLogString +import org.jetbrains.uast.evaluateString +import org.jetbrains.uast.visitor.AbstractUastVisitor /** - * Helper ADT class that facilitates the creation of lint auto fixes + * Helper class that facilitates the creation of lint auto fixes * * Handles "Single" permission checks that should be migrated to @EnforcePermission(...), as well as consecutive checks * that should be migrated to @EnforcePermission(allOf={...}) * * TODO: handle anyOf style annotations */ -sealed class EnforcePermissionFix { - abstract fun locations(): List<Location> - abstract fun javaAnnotationParameter(): String - - fun javaAnnotation(): String = "@$ANNOTATION_ENFORCE_PERMISSION(${javaAnnotationParameter()})" +data class EnforcePermissionFix( + val locations: List<Location>, + val permissionNames: List<String> +) { + val annotation: String + get() { + val quotedPermissions = permissionNames.joinToString(", ") { """"$it"""" } + val annotationParameter = + if (permissionNames.size > 1) "allOf={$quotedPermissions}" else quotedPermissions + return "@$ANNOTATION_ENFORCE_PERMISSION($annotationParameter)" + } companion object { - fun fromCallExpression(callExpression: UCallExpression, context: JavaContext): SingleFix = - SingleFix( - getPermissionCheckLocation(context, callExpression), - getPermissionCheckArgumentValue(callExpression) - ) + /** + * conditionally constructs EnforcePermissionFix from a UCallExpression + * @return EnforcePermissionFix if the called method is annotated with @PermissionMethod, else null + */ + fun fromCallExpression( + context: JavaContext, + callExpression: UCallExpression + ): EnforcePermissionFix? = + if (isPermissionMethodCall(callExpression)) { + EnforcePermissionFix( + listOf(getPermissionCheckLocation(context, callExpression)), + getPermissionCheckValues(callExpression) + ) + } else null - fun maybeAddManifestPrefix(permissionName: String): String = - if (permissionName.contains(".")) permissionName - else "android.Manifest.permission.$permissionName" + + fun compose(individuals: List<EnforcePermissionFix>): EnforcePermissionFix = + EnforcePermissionFix( + individuals.flatMap { it.locations }, + individuals.flatMap { it.permissionNames } + ) /** * Given a permission check, get its proper location @@ -70,49 +87,51 @@ sealed class EnforcePermissionFix { } /** - * Given a permission check and an argument, - * pull out the permission value that is being used + * Given a @PermissionMethod, find arguments annotated with @PermissionName + * and pull out the permission value(s) being used. Also evaluates nested calls + * to @PermissionMethod(s) in the given method's body. */ - private fun getPermissionCheckArgumentValue( - callExpression: UCallExpression, - argumentPosition: Int = 0 - ): String { + private fun getPermissionCheckValues( + callExpression: UCallExpression + ): List<String> { + if (!isPermissionMethodCall(callExpression)) return emptyList() - val identifier = when ( - val argument = callExpression.valueArguments.getOrNull(argumentPosition) - ) { - is UQualifiedReferenceExpression -> when (val selector = argument.selector) { - is USimpleNameReferenceExpression -> - ((selector.resolve() as PsiVariable).computeConstantValue() as String) + val result = mutableSetOf<String>() // protect against duplicate permission values + val visitedCalls = mutableSetOf<UCallExpression>() // don't visit the same call twice + val bfsQueue = ArrayDeque(listOf(callExpression)) - else -> throw RuntimeException( - "Couldn't resolve argument: ${selector.asRecursiveLogString()}" - ) - } + // Breadth First Search - evalutaing nested @PermissionMethod(s) in the available + // source code for @PermissionName(s). + while (bfsQueue.isNotEmpty()) { + val current = bfsQueue.removeFirst() + visitedCalls.add(current) + result.addAll(findPermissions(current)) - is USimpleNameReferenceExpression -> ( - (argument.resolve() as PsiVariable).computeConstantValue() as String) + current.resolve()?.getUMethod()?.accept(object : AbstractUastVisitor() { + override fun visitCallExpression(node: UCallExpression): Boolean { + if (isPermissionMethodCall(node) && node !in visitedCalls) { + bfsQueue.add(node) + } + return false + } + }) + } - is ULiteralExpression -> argument.value as String + return result.toList() + } - else -> throw RuntimeException( - "Couldn't resolve argument: ${argument?.asRecursiveLogString()}" - ) - } + private fun findPermissions( + callExpression: UCallExpression, + ): List<String> { + val indices = callExpression.resolve()?.getUMethod() + ?.uastParameters + ?.filter(::hasPermissionNameAnnotation) + ?.mapNotNull { it.sourcePsi?.parameterIndex() } + ?: emptyList() - return identifier.substringAfterLast(".") + return indices.mapNotNull { + callExpression.getArgumentForParameter(it)?.evaluateString() + } } } } - -data class SingleFix(val location: Location, val permissionName: String) : EnforcePermissionFix() { - override fun locations(): List<Location> = listOf(this.location) - override fun javaAnnotationParameter(): String = maybeAddManifestPrefix(this.permissionName) -} -data class AllOfFix(val checks: List<SingleFix>) : EnforcePermissionFix() { - override fun locations(): List<Location> = this.checks.map { it.location } - override fun javaAnnotationParameter(): String = - "allOf={${ - this.checks.joinToString(", ") { maybeAddManifestPrefix(it.permissionName) } - }}" -} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt new file mode 100644 index 000000000000..edbdd8d2adf3 --- /dev/null +++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.lint.aidl + +import com.android.tools.lint.detector.api.getUMethod +import com.google.android.lint.ANNOTATION_PERMISSION_METHOD +import com.google.android.lint.ANNOTATION_PERMISSION_NAME +import com.google.android.lint.CLASS_STUB +import com.intellij.psi.PsiAnonymousClass +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UParameter + +/** + * Given a UMethod, determine if this method is + * an entrypoint to an interface generated by AIDL, + * returning the interface name if so + */ +fun getContainingAidlInterface(node: UMethod): String? { + if (!isInClassCalledStub(node)) return null + for (superMethod in node.findSuperMethods()) { + for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements + ?: continue) { + if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) { + return superMethod.containingClass?.name + } + } + } + return null +} + +private fun isInClassCalledStub(node: UMethod): Boolean { + (node.containingClass as? PsiAnonymousClass)?.let { + return it.baseClassReference.referenceName == CLASS_STUB + } + return node.containingClass?.extendsList?.referenceElements?.any { + it.referenceName == CLASS_STUB + } ?: false +} + +fun isPermissionMethodCall(callExpression: UCallExpression): Boolean { + val method = callExpression.resolve()?.getUMethod() ?: return false + return hasPermissionMethodAnnotation(method) +} + +fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations + .any { + it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD) + } + +fun hasPermissionNameAnnotation(parameter: UParameter) = parameter.annotations.any { + it.hasQualifiedName(ANNOTATION_PERMISSION_NAME) +} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt index 2cea39423f1d..2c53f390128c 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt +++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt @@ -25,9 +25,6 @@ import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Scope import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner -import com.google.android.lint.CLASS_STUB -import com.google.android.lint.ENFORCE_PERMISSION_METHODS -import com.intellij.psi.PsiAnonymousClass import org.jetbrains.uast.UBlockExpression import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UElement @@ -56,7 +53,7 @@ class ManualPermissionCheckDetector : Detector(), SourceCodeScanner { val body = (node.uastBody as? UBlockExpression) ?: return val fix = accumulateSimplePermissionCheckFixes(body) ?: return - val javaRemoveFixes = fix.locations().map { + val javaRemoveFixes = fix.locations.map { fix() .replace() .reformat(true) @@ -67,7 +64,7 @@ class ManualPermissionCheckDetector : Detector(), SourceCodeScanner { } val javaAnnotateFix = fix() - .annotate(fix.javaAnnotation()) + .annotate(fix.annotation) .range(context.getLocation(node)) .autoFix() .build() @@ -77,7 +74,7 @@ class ManualPermissionCheckDetector : Detector(), SourceCodeScanner { context.report( ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION, - fix.locations().last(), + fix.locations.last(), message, fix().composite(*javaRemoveFixes.toTypedArray(), javaAnnotateFix) ) @@ -97,14 +94,14 @@ class ManualPermissionCheckDetector : Detector(), SourceCodeScanner { */ private fun accumulateSimplePermissionCheckFixes(methodBody: UBlockExpression): EnforcePermissionFix? { - val singleFixes = mutableListOf<SingleFix>() + val singleFixes = mutableListOf<EnforcePermissionFix>() for (expression in methodBody.expressions) { singleFixes.add(getPermissionCheckFix(expression) ?: break) } return when (singleFixes.size) { 0 -> null 1 -> singleFixes[0] - else -> AllOfFix(singleFixes) + else -> EnforcePermissionFix.compose(singleFixes) } } @@ -113,7 +110,7 @@ class ManualPermissionCheckDetector : Detector(), SourceCodeScanner { * the helper for creating a lint auto fix to @EnforcePermission */ private fun getPermissionCheckFix(startingExpression: UElement?): - SingleFix? { + EnforcePermissionFix? { return when (startingExpression) { is UQualifiedReferenceExpression -> getPermissionCheckFix( startingExpression.selector @@ -121,11 +118,8 @@ class ManualPermissionCheckDetector : Detector(), SourceCodeScanner { is UIfExpression -> getPermissionCheckFix(startingExpression.condition) - is UCallExpression -> { - return if (isPermissionCheck(startingExpression)) - EnforcePermissionFix.fromCallExpression(startingExpression, context) - else null - } + is UCallExpression -> return EnforcePermissionFix + .fromCallExpression(context, startingExpression) else -> null } @@ -160,40 +154,5 @@ class ManualPermissionCheckDetector : Detector(), SourceCodeScanner { ), enabledByDefault = false, // TODO: enable once b/241171714 is resolved ) - - private fun isPermissionCheck(callExpression: UCallExpression): Boolean { - val method = callExpression.resolve() ?: return false - val className = method.containingClass?.qualifiedName - return ENFORCE_PERMISSION_METHODS.any { - it.clazz == className && it.name == method.name - } - } - - /** - * given a UMethod, determine if this method is - * an entrypoint to an interface generated by AIDL, - * returning the interface name if so - */ - fun getContainingAidlInterface(node: UMethod): String? { - if (!isInClassCalledStub(node)) return null - for (superMethod in node.findSuperMethods()) { - for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements - ?: continue) { - if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) { - return superMethod.containingClass?.name - } - } - } - return null - } - - private fun isInClassCalledStub(node: UMethod): Boolean { - (node.containingClass as? PsiAnonymousClass)?.let { - return it.baseClassReference.referenceName == CLASS_STUB - } - return node.containingClass?.extendsList?.referenceElements?.any { - it.referenceName == CLASS_STUB - } ?: false - } } } diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt index 1a1c6bc77785..a968f5e6cb1c 100644 --- a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt +++ b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt @@ -19,6 +19,7 @@ package com.google.android.lint.aidl import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFile import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.checks.infrastructure.TestMode import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue @@ -42,7 +43,7 @@ class ManualPermissionCheckDetectorTest : LintDetectorTest() { private Context mContext; @Override public void test() throws android.os.RemoteException { - mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo"); + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); } } """ @@ -53,8 +54,8 @@ class ManualPermissionCheckDetectorTest : LintDetectorTest() { .expect( """ src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation] - mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo"); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ ) @@ -62,9 +63,9 @@ class ManualPermissionCheckDetectorTest : LintDetectorTest() { """ Fix for src/Foo.java line 7: Annotate with @EnforcePermission: @@ -5 +5 - + @android.annotation.EnforcePermission(android.Manifest.permission.READ_CONTACTS) + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") @@ -7 +8 - - mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo"); + - mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); """ ) } @@ -81,7 +82,7 @@ class ManualPermissionCheckDetectorTest : LintDetectorTest() { @Override public void test() throws android.os.RemoteException { mContext.enforceCallingOrSelfPermission( - "android.Manifest.permission.READ_CONTACTS", "foo"); + "android.permission.READ_CONTACTS", "foo"); } }; } @@ -102,10 +103,49 @@ class ManualPermissionCheckDetectorTest : LintDetectorTest() { """ Fix for src/Foo.java line 8: Annotate with @EnforcePermission: @@ -6 +6 - + @android.annotation.EnforcePermission(android.Manifest.permission.READ_CONTACTS) + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") @@ -8 +9 - mContext.enforceCallingOrSelfPermission( - - "android.Manifest.permission.READ_CONTACTS", "foo"); + - "android.permission.READ_CONTACTS", "foo"); + """ + ) + } + + fun testConstantEvaluation() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); + } + } + """ + ).indented(), + *stubs, + manifestStub + ) + .run() + .expect( + """ + src/Foo.java:8: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation] + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); """ ) } @@ -122,9 +162,9 @@ class ManualPermissionCheckDetectorTest : LintDetectorTest() { @Override public void test() throws android.os.RemoteException { mContext.enforceCallingOrSelfPermission( - "android.Manifest.permission.READ_CONTACTS", "foo"); + "android.permission.READ_CONTACTS", "foo"); mContext.enforceCallingOrSelfPermission( - "android.Manifest.permission.WRITE_CONTACTS", "foo"); + "android.permission.WRITE_CONTACTS", "foo"); } }; } @@ -144,13 +184,13 @@ class ManualPermissionCheckDetectorTest : LintDetectorTest() { .expectFixDiffs( """ Fix for src/Foo.java line 10: Annotate with @EnforcePermission: - @@ -6 +6 - + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.READ_CONTACTS, android.Manifest.permission.WRITE_CONTACTS}) + @@ -6 +6 + + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"}) @@ -8 +9 - mContext.enforceCallingOrSelfPermission( - - "android.Manifest.permission.READ_CONTACTS", "foo"); + - "android.permission.READ_CONTACTS", "foo"); - mContext.enforceCallingOrSelfPermission( - - "android.Manifest.permission.WRITE_CONTACTS", "foo"); + - "android.permission.WRITE_CONTACTS", "foo"); """ ) } @@ -166,7 +206,7 @@ class ManualPermissionCheckDetectorTest : LintDetectorTest() { @Override public void test() throws android.os.RemoteException { long uid = Binder.getCallingUid(); - mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo"); + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); } } """ @@ -177,6 +217,149 @@ class ManualPermissionCheckDetectorTest : LintDetectorTest() { .expectClean() } + fun testPermissionHelper() { + lint().skipTestModes(TestMode.PARENTHESIZED).files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.content.pm.PermissionMethod + private void helper() { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + + @Override + public void test() throws android.os.RemoteException { + helper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:14: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation] + helper(); + ~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 14: Annotate with @EnforcePermission: + @@ -12 +12 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -14 +15 + - helper(); + """ + ) + } + + fun testPermissionHelperAllOf() { + lint().skipTestModes(TestMode.PARENTHESIZED).files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.content.pm.PermissionMethod + private void helper() { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + mContext.enforceCallingOrSelfPermission("android.permission.WRITE_CONTACTS", "foo"); + } + + @Override + public void test() throws android.os.RemoteException { + helper(); + mContext.enforceCallingOrSelfPermission("FOO", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:16: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation] + mContext.enforceCallingOrSelfPermission("FOO", "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 16: Annotate with @EnforcePermission: + @@ -13 +13 + + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS", "FOO"}) + @@ -15 +16 + - helper(); + - mContext.enforceCallingOrSelfPermission("FOO", "foo"); + """ + ) + } + + + fun testPermissionHelperNested() { + lint().skipTestModes(TestMode.PARENTHESIZED).files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.content.pm.PermissionMethod + private void helperHelper() { + helper("android.permission.WRITE_CONTACTS"); + } + + @android.content.pm.PermissionMethod + private void helper(@android.content.pm.PermissionName String extraPermission) { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + + @Override + public void test() throws android.os.RemoteException { + helperHelper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:19: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation] + helperHelper(); + ~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 19: Annotate with @EnforcePermission: + @@ -17 +17 + + @android.annotation.EnforcePermission(allOf={"android.permission.WRITE_CONTACTS", "android.permission.READ_CONTACTS"}) + @@ -19 +20 + - helperHelper(); + """ + ) + } + + + companion object { private val aidlStub: TestFile = java( """ @@ -192,7 +375,8 @@ class ManualPermissionCheckDetectorTest : LintDetectorTest() { """ package android.content; public class Context { - public void enforceCallingOrSelfPermission(String permission, String message) {} + @android.content.pm.PermissionMethod + public void enforceCallingOrSelfPermission(@android.content.pm.PermissionName String permission, String message) {} } """ ).indented() @@ -206,6 +390,59 @@ class ManualPermissionCheckDetectorTest : LintDetectorTest() { """ ).indented() - val stubs = arrayOf(aidlStub, contextStub, binderStub) + private val permissionMethodStub: TestFile = java( + """ + package android.content.pm; + + import static java.lang.annotation.ElementType.METHOD; + import static java.lang.annotation.RetentionPolicy.CLASS; + + import java.lang.annotation.Retention; + import java.lang.annotation.Target; + + @Retention(CLASS) + @Target({METHOD}) + public @interface PermissionMethod {} + """ + ).indented() + + private val permissionNameStub: TestFile = java( + """ + package android.content.pm; + + import static java.lang.annotation.ElementType.FIELD; + import static java.lang.annotation.ElementType.LOCAL_VARIABLE; + import static java.lang.annotation.ElementType.METHOD; + import static java.lang.annotation.ElementType.PARAMETER; + import static java.lang.annotation.RetentionPolicy.CLASS; + + import java.lang.annotation.Retention; + import java.lang.annotation.Target; + + @Retention(CLASS) + @Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD}) + public @interface PermissionName {} + """ + ).indented() + + private val manifestStub: TestFile = java( + """ + package android; + + public final class Manifest { + public static final class permission { + public static final String READ_CONTACTS="android.permission.READ_CONTACTS"; + } + } + """.trimIndent() + ) + + val stubs = arrayOf( + aidlStub, + contextStub, + binderStub, + permissionMethodStub, + permissionNameStub + ) } } |