diff options
94 files changed, 4246 insertions, 2164 deletions
diff --git a/api/test-current.txt b/api/test-current.txt index b2ed91b20dfe..16bdefa0e231 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -2372,6 +2372,7 @@ package android.provider { field public static final String LOCATION_IGNORE_SETTINGS_PACKAGE_WHITELIST = "location_ignore_settings_package_whitelist"; field public static final String LOW_POWER_MODE = "low_power"; field public static final String LOW_POWER_MODE_STICKY = "low_power_sticky"; + field public static final String NOTIFICATION_BUBBLES = "notification_bubbles"; field public static final String OVERLAY_DISPLAY_DEVICES = "overlay_display_devices"; field public static final String USE_OPEN_WIFI_PACKAGE = "use_open_wifi_package"; } @@ -2395,7 +2396,7 @@ package android.provider { field public static final String LOCATION_ACCESS_CHECK_DELAY_MILLIS = "location_access_check_delay_millis"; field public static final String LOCATION_ACCESS_CHECK_INTERVAL_MILLIS = "location_access_check_interval_millis"; field public static final String NOTIFICATION_BADGING = "notification_badging"; - field public static final String NOTIFICATION_BUBBLES = "notification_bubbles"; + field @Deprecated public static final String NOTIFICATION_BUBBLES = "notification_bubbles"; field @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public static final String SYNC_PARENT_SOUNDS = "sync_parent_sounds"; field public static final String USER_SETUP_COMPLETE = "user_setup_complete"; field public static final String VOICE_INTERACTION_SERVICE = "voice_interaction_service"; diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto index 273277953569..2c8a5562c979 100644 --- a/cmds/statsd/src/atoms.proto +++ b/cmds/statsd/src/atoms.proto @@ -5939,7 +5939,8 @@ message BubbleUIChanged { optional bool is_ongoing = 10; // Whether the bubble is produced by an app running in foreground. - optional bool is_foreground = 11; + // This is deprecated and the value should be ignored. + optional bool is_foreground = 11 [deprecated = true]; } /** diff --git a/core/java/android/app/ITaskStackListener.aidl b/core/java/android/app/ITaskStackListener.aidl index 61867ea737e7..750020eb5bb8 100644 --- a/core/java/android/app/ITaskStackListener.aidl +++ b/core/java/android/app/ITaskStackListener.aidl @@ -178,6 +178,13 @@ oneway interface ITaskStackListener { */ void onSingleTaskDisplayDrawn(int displayId); + /* + * Called when the last task is removed from a display which can only contain one task. + * + * @param displayId the id of the display from which the window is removed. + */ + void onSingleTaskDisplayEmpty(int displayId); + /** * Called when a task is reparented to a stack on a different display. * diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index ac531186b974..f065ff791dce 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -8542,24 +8542,34 @@ public class Notification implements Parcelable * If set and the app creating the bubble is in the foreground, the bubble will be posted * in its expanded state, with the contents of {@link #getIntent()} in a floating window. * - * <p>If the app creating the bubble is not in the foreground this flag has no effect.</p> + * <p>This flag has no effect if the app posting the bubble is not in the foreground. + * The app is considered foreground if it is visible and on the screen, note that + * a foreground service does not qualify. + * </p> * * <p>Generally this flag should only be set if the user has performed an action to request * or create a bubble.</p> + * + * @hide */ - private static final int FLAG_AUTO_EXPAND_BUBBLE = 0x00000001; + public static final int FLAG_AUTO_EXPAND_BUBBLE = 0x00000001; /** * If set and the app posting the bubble is in the foreground, the bubble will * be posted <b>without</b> the associated notification in the notification shade. * - * <p>If the app posting the bubble is not in the foreground this flag has no effect.</p> + * <p>This flag has no effect if the app posting the bubble is not in the foreground. + * The app is considered foreground if it is visible and on the screen, note that + * a foreground service does not qualify. + * </p> * * <p>Generally this flag should only be set if the user has performed an action to request * or create a bubble, or if the user has seen the content in the notification and the * notification is no longer relevant.</p> + * + * @hide */ - private static final int FLAG_SUPPRESS_NOTIFICATION = 0x00000002; + public static final int FLAG_SUPPRESS_NOTIFICATION = 0x00000002; private BubbleMetadata(PendingIntent expandIntent, PendingIntent deleteIntent, Icon icon, int height, @DimenRes int heightResId) { @@ -8675,11 +8685,21 @@ public class Notification implements Parcelable out.writeInt(mDesiredHeightResId); } - private void setFlags(int flags) { + /** + * @hide + */ + public void setFlags(int flags) { mFlags = flags; } /** + * @hide + */ + public int getFlags() { + return mFlags; + } + + /** * Builder to construct a {@link BubbleMetadata} object. */ public static final class Builder { @@ -8795,6 +8815,8 @@ public class Notification implements Parcelable * {@link #getIntent()} in a floating window). * * <p>This flag has no effect if the app posting the bubble is not in the foreground. + * The app is considered foreground if it is visible and on the screen, note that + * a foreground service does not qualify. * </p> * * <p>Generally, this flag should only be set if the user has performed an action to @@ -8813,6 +8835,8 @@ public class Notification implements Parcelable * the notification shade. * * <p>This flag has no effect if the app posting the bubble is not in the foreground. + * The app is considered foreground if it is visible and on the screen, note that + * a foreground service does not qualify. * </p> * * <p>Generally, this flag should only be set if the user has performed an action to diff --git a/core/java/android/app/TaskStackListener.java b/core/java/android/app/TaskStackListener.java index e3a0e11c3c68..46045faecbd4 100644 --- a/core/java/android/app/TaskStackListener.java +++ b/core/java/android/app/TaskStackListener.java @@ -180,6 +180,10 @@ public abstract class TaskStackListener extends ITaskStackListener.Stub { } @Override + public void onSingleTaskDisplayEmpty(int displayId) throws RemoteException { + } + + @Override public void onTaskDisplayChanged(int taskId, int newDisplayId) throws RemoteException { } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index dd3942e084cd..7115da2bb531 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7910,8 +7910,10 @@ public final class Settings { * Whether the notification bubbles are globally enabled * The value is boolean (1 or 0). * @hide + * @deprecated use {@link Global#NOTIFICATION_BUBBLES} instead. */ @TestApi + @Deprecated public static final String NOTIFICATION_BUBBLES = "notification_bubbles"; /** @@ -8235,6 +8237,14 @@ public final class Settings { public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/global"); /** + * Whether the notification bubbles are globally enabled + * The value is boolean (1 or 0). + * @hide + */ + @TestApi + public static final String NOTIFICATION_BUBBLES = "notification_bubbles"; + + /** * Whether users are allowed to add more users or guest from lockscreen. * <p> * Type: int diff --git a/core/proto/android/providers/settings/global.proto b/core/proto/android/providers/settings/global.proto index f2ca0a4e5fad..a568c13d7dde 100644 --- a/core/proto/android/providers/settings/global.proto +++ b/core/proto/android/providers/settings/global.proto @@ -688,6 +688,7 @@ message GlobalSettingsProto { // Configuration options for smart replies and smart actions in notifications. This is // encoded as a key=value list separated by commas. optional SettingProto smart_suggestions_in_notifications_flags = 5 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto bubbles = 6 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Notification notification = 82; diff --git a/packages/CarSystemUI/src/com/android/systemui/car/CarNotificationInterruptionStateProvider.java b/packages/CarSystemUI/src/com/android/systemui/car/CarNotificationInterruptionStateProvider.java index ec40cc377594..afd722ba0091 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/CarNotificationInterruptionStateProvider.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/CarNotificationInterruptionStateProvider.java @@ -18,6 +18,8 @@ package com.android.systemui.car; import android.content.Context; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.notification.NotificationFilter; import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -30,8 +32,10 @@ public class CarNotificationInterruptionStateProvider extends NotificationInterruptionStateProvider { @Inject - public CarNotificationInterruptionStateProvider(Context context) { - super(context); + public CarNotificationInterruptionStateProvider(Context context, + NotificationFilter filter, + StatusBarStateController stateController) { + super(context, filter, stateController); } @Override diff --git a/packages/SettingsLib/SearchWidget/res/values-pt-rPT/strings.xml b/packages/SettingsLib/SearchWidget/res/values-pt-rPT/strings.xml index 363d88544a03..7846be161c0f 100644 --- a/packages/SettingsLib/SearchWidget/res/values-pt-rPT/strings.xml +++ b/packages/SettingsLib/SearchWidget/res/values-pt-rPT/strings.xml @@ -17,5 +17,5 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="search_menu" msgid="1604061903696928905">"Pesquisar definições"</string> + <string name="search_menu" msgid="1604061903696928905">"Definições de pesquisa"</string> </resources> diff --git a/packages/SettingsLib/res/values-bn/strings.xml b/packages/SettingsLib/res/values-bn/strings.xml index f1fc9f9e3d4e..13890e029274 100644 --- a/packages/SettingsLib/res/values-bn/strings.xml +++ b/packages/SettingsLib/res/values-bn/strings.xml @@ -452,7 +452,7 @@ <string name="cancel" msgid="6859253417269739139">"বাতিল"</string> <string name="okay" msgid="1997666393121016642">"ঠিক আছে"</string> <string name="zen_mode_enable_dialog_turn_on" msgid="8287824809739581837">"চালু করুন"</string> - <string name="zen_mode_settings_turn_on_dialog_title" msgid="2297134204747331078">"\'বিরক্ত করবে না\' মোড চালু করুন"</string> + <string name="zen_mode_settings_turn_on_dialog_title" msgid="2297134204747331078">"\'বিরক্ত করবেন না\' মোড চালু করুন"</string> <string name="zen_mode_settings_summary_off" msgid="6119891445378113334">"কখনও নয়"</string> <string name="zen_interruption_level_priority" msgid="2078370238113347720">"শুধুমাত্র অগ্রাধিকার"</string> <string name="zen_mode_and_condition" msgid="4927230238450354412">"<xliff:g id="ZEN_MODE">%1$s</xliff:g>. <xliff:g id="EXIT_CONDITION">%2$s</xliff:g>"</string> diff --git a/packages/SettingsLib/res/values-es-rUS/strings.xml b/packages/SettingsLib/res/values-es-rUS/strings.xml index 7874aeb02996..beb1ac546949 100644 --- a/packages/SettingsLib/res/values-es-rUS/strings.xml +++ b/packages/SettingsLib/res/values-es-rUS/strings.xml @@ -141,7 +141,7 @@ <string name="data_usage_uninstalled_apps" msgid="614263770923231598">"Aplicaciones eliminadas"</string> <string name="data_usage_uninstalled_apps_users" msgid="7986294489899813194">"Aplicaciones y usuarios eliminados"</string> <string name="data_usage_ota" msgid="5377889154805560860">"Actualizaciones del sistema"</string> - <string name="tether_settings_title_usb" msgid="6688416425801386511">"Conexión a red por USB"</string> + <string name="tether_settings_title_usb" msgid="6688416425801386511">"Conexión USB"</string> <string name="tether_settings_title_wifi" msgid="3277144155960302049">"Hotspot portátil"</string> <string name="tether_settings_title_bluetooth" msgid="355855408317564420">"Conexión Bluetooth"</string> <string name="tether_settings_title_usb_bluetooth" msgid="5355828977109785001">"Compartir conexión"</string> @@ -286,7 +286,7 @@ <string name="wait_for_debugger_summary" msgid="1766918303462746804">"Esperar que se conecte el depurador para iniciar la aplicación"</string> <string name="debug_input_category" msgid="1811069939601180246">"Entrada"</string> <string name="debug_drawing_category" msgid="6755716469267367852">"Dibujo"</string> - <string name="debug_hw_drawing_category" msgid="6220174216912308658">"Procesamiento acelerado mediante hardware"</string> + <string name="debug_hw_drawing_category" msgid="6220174216912308658">"Representación acelerada mediante hardware"</string> <string name="media_category" msgid="4388305075496848353">"Multimedia"</string> <string name="debug_monitoring_category" msgid="7640508148375798343">"Supervisión"</string> <string name="strict_mode" msgid="1938795874357830695">"Modo estricto"</string> diff --git a/packages/SettingsLib/res/values-hi/arrays.xml b/packages/SettingsLib/res/values-hi/arrays.xml index 3d9a78e99204..5ad9b0191c77 100644 --- a/packages/SettingsLib/res/values-hi/arrays.xml +++ b/packages/SettingsLib/res/values-hi/arrays.xml @@ -76,7 +76,7 @@ <item msgid="3422726142222090896">"avrcp16"</item> </string-array> <string-array name="bluetooth_a2dp_codec_titles"> - <item msgid="7065842274271279580">"सिस्टम से चुने जाने का उपयोग करें (डिफ़ॉल्ट)"</item> + <item msgid="7065842274271279580">"सिस्टम चयन का उपयोग करें (डिफ़ॉल्ट)"</item> <item msgid="7539690996561263909">"SBC"</item> <item msgid="686685526567131661">"AAC"</item> <item msgid="5254942598247222737">"<xliff:g id="QUALCOMM">Qualcomm®</xliff:g> <xliff:g id="APTX">aptX™</xliff:g> ऑडियो"</item> @@ -86,7 +86,7 @@ <item msgid="3304843301758635896">"वैकल्पिक कोडेक अक्षम करें"</item> </string-array> <string-array name="bluetooth_a2dp_codec_summaries"> - <item msgid="5062108632402595000">"सिस्टम से चुने जाने का उपयोग करें (डिफ़ॉल्ट)"</item> + <item msgid="5062108632402595000">"सिस्टम चयन का उपयोग करें (डिफ़ॉल्ट)"</item> <item msgid="6898329690939802290">"SBC"</item> <item msgid="6839647709301342559">"AAC"</item> <item msgid="7848030269621918608">"<xliff:g id="QUALCOMM">Qualcomm®</xliff:g> <xliff:g id="APTX">aptX™</xliff:g> ऑडियो"</item> @@ -96,38 +96,38 @@ <item msgid="741805482892725657">"वैकल्पिक कोडेक अक्षम करें"</item> </string-array> <string-array name="bluetooth_a2dp_codec_sample_rate_titles"> - <item msgid="3093023430402746802">"सिस्टम से चुने जाने का उपयोग करें (डिफ़ॉल्ट)"</item> + <item msgid="3093023430402746802">"सिस्टम चयन का उपयोग करें (डिफ़ॉल्ट)"</item> <item msgid="8895532488906185219">"44.1 kHz"</item> <item msgid="2909915718994807056">"48.0 kHz"</item> <item msgid="3347287377354164611">"88.2 kHz"</item> <item msgid="1234212100239985373">"96.0 kHz"</item> </string-array> <string-array name="bluetooth_a2dp_codec_sample_rate_summaries"> - <item msgid="3214516120190965356">"सिस्टम से चुने जाने का उपयोग करें (डिफ़ॉल्ट)"</item> + <item msgid="3214516120190965356">"सिस्टम चयन का उपयोग करें (डिफ़ॉल्ट)"</item> <item msgid="4482862757811638365">"44.1 kHz"</item> <item msgid="354495328188724404">"48.0 kHz"</item> <item msgid="7329816882213695083">"88.2 kHz"</item> <item msgid="6967397666254430476">"96.0 kHz"</item> </string-array> <string-array name="bluetooth_a2dp_codec_bits_per_sample_titles"> - <item msgid="2684127272582591429">"सिस्टम से चुने जाने का उपयोग करें (डिफ़ॉल्ट)"</item> + <item msgid="2684127272582591429">"सिस्टम चयन का उपयोग करें (डिफ़ॉल्ट)"</item> <item msgid="5618929009984956469">"16 बिट/नमूना"</item> <item msgid="3412640499234627248">"24 बिट/नमूना"</item> <item msgid="121583001492929387">"32 बिट/नमूना"</item> </string-array> <string-array name="bluetooth_a2dp_codec_bits_per_sample_summaries"> - <item msgid="1081159789834584363">"सिस्टम से चुने जाने का उपयोग करें (डिफ़ॉल्ट)"</item> + <item msgid="1081159789834584363">"सिस्टम चयन का उपयोग करें (डिफ़ॉल्ट)"</item> <item msgid="4726688794884191540">"16 बिट/नमूना"</item> <item msgid="305344756485516870">"24 बिट/नमूना"</item> <item msgid="244568657919675099">"32 बिट/नमूना"</item> </string-array> <string-array name="bluetooth_a2dp_codec_channel_mode_titles"> - <item msgid="5226878858503393706">"सिस्टम से चुने जाने का उपयोग करें (डिफ़ॉल्ट)"</item> + <item msgid="5226878858503393706">"सिस्टम चयन का उपयोग करें (डिफ़ॉल्ट)"</item> <item msgid="4106832974775067314">"मोनो"</item> <item msgid="5571632958424639155">"स्टीरियो"</item> </string-array> <string-array name="bluetooth_a2dp_codec_channel_mode_summaries"> - <item msgid="4118561796005528173">"सिस्टम चुनाव का उपयोग करें (डिफ़ॉल्ट)"</item> + <item msgid="4118561796005528173">"सिस्टम चयन का उपयोग करें (डिफ़ॉल्ट)"</item> <item msgid="8900559293912978337">"मोनो"</item> <item msgid="8883739882299884241">"स्टीरियो"</item> </string-array> diff --git a/packages/SettingsLib/res/values-hi/strings.xml b/packages/SettingsLib/res/values-hi/strings.xml index 3a20d04d7ab2..5d512a84ef99 100644 --- a/packages/SettingsLib/res/values-hi/strings.xml +++ b/packages/SettingsLib/res/values-hi/strings.xml @@ -46,7 +46,7 @@ <string name="wifi_limited_connection" msgid="7717855024753201527">"सीमित कनेक्शन"</string> <string name="wifi_status_no_internet" msgid="5784710974669608361">"इंटरनेट कनेक्शन नहीं है"</string> <string name="wifi_status_sign_in_required" msgid="123517180404752756">"साइन इन करना ज़रूरी है"</string> - <string name="wifi_ap_unable_to_handle_new_sta" msgid="5348824313514404541">"ऐक्सेस पॉइंट फ़िलहाल भरा हुआ है"</string> + <string name="wifi_ap_unable_to_handle_new_sta" msgid="5348824313514404541">"एक्सेस पॉइंट फ़िलहाल भरा हुआ है"</string> <string name="connected_via_carrier" msgid="7583780074526041912">"%1$s के ज़रिए कनेक्ट"</string> <string name="available_via_carrier" msgid="1469036129740799053">"%1$s के ज़रिए उपलब्ध"</string> <string name="osu_opening_provider" msgid="5488997661548640424">"<xliff:g id="PASSPOINTPROVIDER">%1$s</xliff:g> खोला जा रहा है"</string> @@ -68,7 +68,7 @@ <string name="bluetooth_pairing" msgid="1426882272690346242">"युग्मित कर रहा है…"</string> <string name="bluetooth_connected_no_headset" msgid="616068069034994802">"जुड़ गया (फ़ोन के ऑडियो को छोड़कर)<xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g>"</string> <string name="bluetooth_connected_no_a2dp" msgid="3736431800395923868">"जुड़ गया (मीडिया ऑडियो को छोड़कर)<xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g>"</string> - <string name="bluetooth_connected_no_map" msgid="3200033913678466453">"जुड़ गया (मैसेज का ऐक्सेस नहीं)<xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g>"</string> + <string name="bluetooth_connected_no_map" msgid="3200033913678466453">"जुड़ गया (मैसेज का एक्सेस नहीं)<xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g>"</string> <string name="bluetooth_connected_no_headset_no_a2dp" msgid="2047403011284187056">"जुड़ गया (फ़ोन या मीडिया ऑडियो को छोड़कर)<xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g>"</string> <string name="bluetooth_connected_battery_level" msgid="5162924691231307748">"जुड़ गया, बैटरी का लेवल <xliff:g id="BATTERY_LEVEL_AS_PERCENTAGE">%1$s</xliff:g><xliff:g id="ACTIVE_DEVICE">%2$s</xliff:g>"</string> <string name="bluetooth_connected_no_headset_battery_level" msgid="1610296229139400266">"जुड़ गया (फ़ोन के ऑडियो को छोड़कर), बैटरी का लेवल <xliff:g id="BATTERY_LEVEL_AS_PERCENTAGE">%1$s</xliff:g><xliff:g id="ACTIVE_DEVICE">%2$s</xliff:g>"</string> @@ -88,7 +88,7 @@ <string name="bluetooth_profile_pbap_summary" msgid="6605229608108852198">"संपर्क साझाकरण के लिए उपयोग करें"</string> <string name="bluetooth_profile_pan_nap" msgid="8429049285027482959">"इंटरनेट कनेक्शन साझाकरण"</string> <string name="bluetooth_profile_map" msgid="1019763341565580450">"लेख संदेश"</string> - <string name="bluetooth_profile_sap" msgid="5764222021851283125">"सिम ऐक्सेस"</string> + <string name="bluetooth_profile_sap" msgid="5764222021851283125">"सिम एक्सेस"</string> <string name="bluetooth_profile_a2dp_high_quality" msgid="5444517801472820055">"HD ऑडियो: <xliff:g id="CODEC_NAME">%1$s</xliff:g>"</string> <string name="bluetooth_profile_a2dp_high_quality_unknown_codec" msgid="8510588052415438887">"HD ऑडियो"</string> <string name="bluetooth_profile_hearing_aid" msgid="6680721080542444257">"सुनने में मदद करने वाले डिवाइस"</string> @@ -200,7 +200,7 @@ <string name="development_settings_not_available" msgid="4308569041701535607">"यह उपयोगकर्ता, डेवलपर के लिए सेटिंग और टूल का इस्तेमाल नहीं कर सकता"</string> <string name="vpn_settings_not_available" msgid="956841430176985598">"VPN सेटिंग इस उपयोगकर्ता के लिए उपलब्ध नहीं हैं"</string> <string name="tethering_settings_not_available" msgid="6765770438438291012">"टेदरिंग सेटिंग इस उपयोगकर्ता के लिए उपलब्ध नहीं हैं"</string> - <string name="apn_settings_not_available" msgid="7873729032165324000">"ऐक्सेस पॉइंट के नाम की सेटिंग इस उपयोगकर्ता के लिए मौजूद नहीं हैं"</string> + <string name="apn_settings_not_available" msgid="7873729032165324000">"एक्सेस पॉइंट के नाम की सेटिंग इस उपयोगकर्ता के लिए मौजूद नहीं हैं"</string> <string name="enable_adb" msgid="7982306934419797485">"USB डीबग करना"</string> <string name="enable_adb_summary" msgid="4881186971746056635">"डीबग मोड जब USB कनेक्ट किया गया हो"</string> <string name="clear_adb_keys" msgid="4038889221503122743">"USB डीबग करने की मंज़ूरी रद्द करें"</string> @@ -361,7 +361,7 @@ <string name="runningservices_settings_summary" msgid="854608995821032748">"इस समय चल रही सेवाओं को देखें और नियंत्रित करें"</string> <string name="select_webview_provider_title" msgid="4628592979751918907">"वेबव्यू लागू करें"</string> <string name="select_webview_provider_dialog_title" msgid="4370551378720004872">"वेबव्यू सेट करें"</string> - <string name="select_webview_provider_toast_text" msgid="5466970498308266359">"यह चुनाव अब मान्य नहीं है. दोबारा कोशिश करें."</string> + <string name="select_webview_provider_toast_text" msgid="5466970498308266359">"यह चयन अब मान्य नहीं है. पुनः प्रयास करें."</string> <string name="convert_to_file_encryption" msgid="3060156730651061223">"फ़ाइल आधारित सुरक्षित करने के तरीके में बदलें"</string> <string name="convert_to_file_encryption_enabled" msgid="2861258671151428346">"रूपांतरित करें..."</string> <string name="convert_to_file_encryption_done" msgid="7859766358000523953">"फ़ाइल पहले से एन्क्रिप्ट की हुई है"</string> @@ -414,7 +414,7 @@ <string name="disabled" msgid="9206776641295849915">"बंद किया गया"</string> <string name="external_source_trusted" msgid="2707996266575928037">"अनुमति है"</string> <string name="external_source_untrusted" msgid="2677442511837596726">"अनुमति नहीं है"</string> - <string name="install_other_apps" msgid="6986686991775883017">"अनजान ऐप्लिकेशन इंस्टॉल करने का ऐक्सेस"</string> + <string name="install_other_apps" msgid="6986686991775883017">"अनजान ऐप्लिकेशन इंस्टॉल करने का एक्सेस"</string> <string name="home" msgid="3256884684164448244">"सेटिंग का होम पेज"</string> <string-array name="battery_labels"> <item msgid="8494684293649631252">"0%"</item> diff --git a/packages/SettingsLib/res/values-hy/arrays.xml b/packages/SettingsLib/res/values-hy/arrays.xml index 7368f1d105f2..5cffafed6689 100644 --- a/packages/SettingsLib/res/values-hy/arrays.xml +++ b/packages/SettingsLib/res/values-hy/arrays.xml @@ -43,7 +43,7 @@ <item msgid="8937994881315223448">"Միացված է <xliff:g id="NETWORK_NAME">%1$s</xliff:g>-ին"</item> <item msgid="1330262655415760617">"Անջատված"</item> <item msgid="7698638434317271902">"Անջատվում է <xliff:g id="NETWORK_NAME">%1$s</xliff:g>-ից…"</item> - <item msgid="197508606402264311">"Անջատված է"</item> + <item msgid="197508606402264311">"Անջատած է"</item> <item msgid="8578370891960825148">"Անհաջող"</item> <item msgid="5660739516542454527">"Արգելափակված"</item> <item msgid="1805837518286731242">"Վատ ցանցից ժամանակավոր խուսափում"</item> diff --git a/packages/SettingsLib/res/values-hy/strings.xml b/packages/SettingsLib/res/values-hy/strings.xml index a74b4ae3ae9e..bf587405c668 100644 --- a/packages/SettingsLib/res/values-hy/strings.xml +++ b/packages/SettingsLib/res/values-hy/strings.xml @@ -263,7 +263,7 @@ <string name="debug_view_attributes" msgid="6485448367803310384">"Միացնել ցուցադրման հատկանիշների ստուգումը"</string> <string name="mobile_data_always_on_summary" msgid="8149773901431697910">"Միշտ ակտիվացրած պահել բջջային տվյալները, նույնիսկ Wi‑Fi-ը միացրած ժամանակ (ցանցերի միջև արագ փոխարկման համար):"</string> <string name="tethering_hardware_offload_summary" msgid="7726082075333346982">"Օգտագործել սարքակազմի արագացման միացումը, եթե հասանելի է"</string> - <string name="adb_warning_title" msgid="6234463310896563253">"Թույլատրե՞լ USB վրիպազերծումը:"</string> + <string name="adb_warning_title" msgid="6234463310896563253">"Թույլատրե՞լ USB-ի վրիպազերծումը:"</string> <string name="adb_warning_message" msgid="7316799925425402244">"USB վրիպազերծումը միայն ծրագրավորման նպատակների համար է: Օգտագործեք այն ձեր համակարգչից տվյալները ձեր սարք պատճենելու համար, առանց ծանուցման ձեր սարքի վրա ծրագրեր տեղադրելու և տվյալների մատյանը ընթերցելու համար:"</string> <string name="adb_keys_warning_message" msgid="5659849457135841625">"Փակե՞լ USB-ի վրիպազերծման մուտքը` անջատելով այն բոլոր համակարգիչներից, որտեղ նախկինում թույլատրել էիք:"</string> <string name="dev_settings_warning_title" msgid="7244607768088540165">"Ընդունե՞լ ծրագրավորման կարգավորումներ:"</string> diff --git a/packages/SettingsLib/res/values-ne/strings.xml b/packages/SettingsLib/res/values-ne/strings.xml index f1934c37369e..fd45cb0a1f3c 100644 --- a/packages/SettingsLib/res/values-ne/strings.xml +++ b/packages/SettingsLib/res/values-ne/strings.xml @@ -248,7 +248,7 @@ <string name="wifi_display_certification_summary" msgid="1155182309166746973">"ताररहित प्रदर्शन प्रमाणीकरणका लागि विकल्पहरू देखाउनुहोस्"</string> <string name="wifi_verbose_logging_summary" msgid="6615071616111731958">"Wi-Fi लग स्तर बढाउनुहोस्, Wi-Fi चयनकर्तामा प्रति SSID RSSI देखाइन्छ"</string> <string name="wifi_scan_throttling_summary" msgid="4461922728822495763">"ब्याट्रीको खपत कम गरी नेटवर्कको कार्यसम्पादनमा सुधार गर्दछ"</string> - <string name="wifi_metered_label" msgid="4514924227256839725">"सशुल्क वाइफाइ"</string> + <string name="wifi_metered_label" msgid="4514924227256839725">"मिटर गरिएको जडान भनी चिन्ह लगाइएको"</string> <string name="wifi_unmetered_label" msgid="6124098729457992931">"मिटर नगरिएको"</string> <string name="select_logd_size_title" msgid="7433137108348553508">"लगर बफर आकारहरू"</string> <string name="select_logd_size_dialog_title" msgid="1206769310236476760">"लग बफर प्रति लगर आकार चयन गर्नुहोस्"</string> diff --git a/packages/SettingsLib/res/values-ta/strings.xml b/packages/SettingsLib/res/values-ta/strings.xml index 57e690d46b23..cf442b73e721 100644 --- a/packages/SettingsLib/res/values-ta/strings.xml +++ b/packages/SettingsLib/res/values-ta/strings.xml @@ -159,7 +159,7 @@ <string name="tts_default_pitch_title" msgid="6135942113172488671">"ஒலித்திறன்"</string> <string name="tts_default_pitch_summary" msgid="1944885882882650009">"உருவாக்கப்படும் பேச்சின் டோன் பாதிக்கப்படும்"</string> <string name="tts_default_lang_title" msgid="8018087612299820556">"மொழி"</string> - <string name="tts_lang_use_system" msgid="2679252467416513208">"அமைப்பின் மொழியைப் பயன்படுத்தவும்"</string> + <string name="tts_lang_use_system" msgid="2679252467416513208">"அமைப்பின் மொழியில்"</string> <string name="tts_lang_not_selected" msgid="7395787019276734765">"மொழி தேர்ந்தெடுக்கப்படவில்லை"</string> <string name="tts_default_lang_summary" msgid="5219362163902707785">"பேசப்படும் உரைக்கு மொழி சார்ந்த குரலை அமைக்கிறது"</string> <string name="tts_play_example_title" msgid="7094780383253097230">"எடுத்துக்காட்டைக் கவனிக்கவும்"</string> diff --git a/packages/SettingsLib/res/values-vi/strings.xml b/packages/SettingsLib/res/values-vi/strings.xml index 5c06c80a049c..4e2d6fa76ad0 100644 --- a/packages/SettingsLib/res/values-vi/strings.xml +++ b/packages/SettingsLib/res/values-vi/strings.xml @@ -201,7 +201,7 @@ <string name="vpn_settings_not_available" msgid="956841430176985598">"Cài đặt VPN không khả dụng cho người dùng này"</string> <string name="tethering_settings_not_available" msgid="6765770438438291012">"Cài đặt chia sẻ kết nối không khả dụng cho người dùng này"</string> <string name="apn_settings_not_available" msgid="7873729032165324000">"Cài đặt tên điểm truy cập không khả dụng cho người dùng này"</string> - <string name="enable_adb" msgid="7982306934419797485">"Gỡ lỗi qua USB"</string> + <string name="enable_adb" msgid="7982306934419797485">"Gỡ lỗi USB"</string> <string name="enable_adb_summary" msgid="4881186971746056635">"Bật chế độ gỡ lỗi khi kết nối USB"</string> <string name="clear_adb_keys" msgid="4038889221503122743">"Thu hồi ủy quyền gỡ lỗi USB"</string> <string name="bugreport_in_power" msgid="7923901846375587241">"Phím tắt báo cáo lỗi"</string> @@ -263,7 +263,7 @@ <string name="debug_view_attributes" msgid="6485448367803310384">"Cho phép kiểm tra thuộc tính của chế độ xem"</string> <string name="mobile_data_always_on_summary" msgid="8149773901431697910">"Luôn bật dữ liệu di động ngay cả khi Wi-Fi đang hoạt động (để chuyển đổi mạng nhanh)."</string> <string name="tethering_hardware_offload_summary" msgid="7726082075333346982">"Sử dụng tính năng tăng tốc phần cứng khi chia sẻ kết nối nếu có"</string> - <string name="adb_warning_title" msgid="6234463310896563253">"Cho phép gỡ lỗi qua USB?"</string> + <string name="adb_warning_title" msgid="6234463310896563253">"Cho phép gỡ lỗi USB?"</string> <string name="adb_warning_message" msgid="7316799925425402244">"Gỡ lỗi USB chỉ dành cho mục đích phát triển. Hãy sử dụng tính năng này để sao chép dữ liệu giữa máy tính và thiết bị của bạn, cài đặt ứng dụng trên thiết bị của bạn mà không thông báo và đọc dữ liệu nhật ký."</string> <string name="adb_keys_warning_message" msgid="5659849457135841625">"Thu hồi quyền truy cập gỡ lỗi USB từ tất cả máy tính mà bạn đã ủy quyền trước đó?"</string> <string name="dev_settings_warning_title" msgid="7244607768088540165">"Cho phép cài đặt phát triển?"</string> diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java index 0c49f635f5bd..1527de1a1d17 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java @@ -69,5 +69,6 @@ public class GlobalSettings { Settings.Global.ZEN_DURATION, Settings.Global.CHARGING_VIBRATION_ENABLED, Settings.Global.AWARE_ALLOWED, + Settings.Global.NOTIFICATION_BUBBLES, }; } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 00b2563f559b..365923ea8643 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -1114,6 +1114,9 @@ class SettingsProtoDumpUtil { Settings.Global.NOTIFICATION_SNOOZE_OPTIONS, GlobalSettingsProto.Notification.SNOOZE_OPTIONS); dumpSetting(s, p, + Settings.Global.NOTIFICATION_BUBBLES, + GlobalSettingsProto.Notification.BUBBLES); + dumpSetting(s, p, Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS, GlobalSettingsProto.Notification.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS); dumpSetting(s, p, @@ -2225,7 +2228,7 @@ class SettingsProtoDumpUtil { Settings.Secure.NOTIFICATION_BADGING, SecureSettingsProto.Notification.BADGING); dumpSetting(s, p, - Settings.Secure.NOTIFICATION_BUBBLES, + Settings.Global.NOTIFICATION_BUBBLES, SecureSettingsProto.Notification.BUBBLES); dumpSetting(s, p, Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 4d71e72b59d6..32fc7ff978cd 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -3186,7 +3186,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 182; + private static final int SETTINGS_VERSION = 183; private final int mUserId; @@ -4203,19 +4203,7 @@ public class SettingsProvider extends ContentProvider { if (currentVersion == 173) { // Version 173: Set the default value for Secure Settings: NOTIFICATION_BUBBLES - - final SettingsState secureSettings = getSecureSettingsLocked(userId); - - final Setting bubblesSetting = secureSettings.getSettingLocked( - Secure.NOTIFICATION_BUBBLES); - - if (bubblesSetting.isNull()) { - secureSettings.insertSettingLocked(Secure.NOTIFICATION_BUBBLES, - getContext().getResources().getBoolean( - R.bool.def_notification_bubbles) ? "1" : "0", null, - true, SettingsState.SYSTEM_PACKAGE_NAME); - } - + // Removed. Moved NOTIFICATION_BUBBLES to Global Settings. currentVersion = 174; } @@ -4339,14 +4327,9 @@ public class SettingsProvider extends ContentProvider { if (currentVersion == 179) { // Version 178: Reset the default for Secure Settings: NOTIFICATION_BUBBLES // This is originally set in version 173, however, the default value changed - // so this step is to ensure the value is updated to the correct defaulte - final SettingsState secureSettings = getSecureSettingsLocked(userId); - - secureSettings.insertSettingLocked(Secure.NOTIFICATION_BUBBLES, - getContext().getResources().getBoolean( - R.bool.def_notification_bubbles) ? "1" : "0", null, - true, SettingsState.SYSTEM_PACKAGE_NAME); + // so this step is to ensure the value is updated to the correct default. + // Removed. Moved NOTIFICATION_BUBBLES to Global Settings. currentVersion = 180; } @@ -4400,6 +4383,20 @@ public class SettingsProvider extends ContentProvider { currentVersion = 182; } + if (currentVersion == 182) { + // Remove secure bubble settings. + getSecureSettingsLocked(userId).deleteSettingLocked( + Secure.NOTIFICATION_BUBBLES); + + // Add global bubble settings. + getGlobalSettingsLocked().insertSettingLocked(Global.NOTIFICATION_BUBBLES, + getContext().getResources().getBoolean( + R.bool.def_notification_bubbles) ? "1" : "0", null /* tag */, + true /* makeDefault */, SettingsState.SYSTEM_PACKAGE_NAME); + + currentVersion = 183; + } + // vXXX: Add new settings above this point. if (currentVersion != newVersion) { diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 4c52b1324781..3e5d72077f8b 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -114,6 +114,7 @@ android_library { "androidx.lifecycle_lifecycle-extensions", "androidx.dynamicanimation_dynamicanimation", "androidx-constraintlayout_constraintlayout", + "iconloader_base", "SystemUI-tags", "SystemUI-proto", "metrics-helper-lib", diff --git a/packages/SystemUI/docs/physics-animation-layout.md b/packages/SystemUI/docs/physics-animation-layout.md index 488c4657333f..de2ee9e04b30 100644 --- a/packages/SystemUI/docs/physics-animation-layout.md +++ b/packages/SystemUI/docs/physics-animation-layout.md @@ -1,7 +1,11 @@ # Physics Animation Layout ## Overview -**PhysicsAnimationLayout** works with an implementation of **PhysicsAnimationController** to construct and maintain physics animations for each of its child views. During the initial construction of the animations, the layout queries the controller for configuration settings such as which properties to animate, which animations to chain together, and what stiffness or bounciness to use. Once the animations are built to the controller’s specifications, the controller can then ask the layout to start, stop and manipulate them arbitrarily to achieve any desired animation effect. The controller is notified whenever children are added or removed from the layout, so that it can animate their entrance or exit, respectively. +**PhysicsAnimationLayout** works with implementations of **PhysicsAnimationController** to configure and run physics-based animations for each of its child views. During the initial construction of the animations, the layout queries the controller for basic configuration settings such as which properties to animate, which animations to chain together, and the default physics parameters to use. + +Once the animations are built, the controller can access **PhysicsPropertyAnimator** instances to run them. The animator behaves similarly to the familiar `ViewPropertyAnimator`, with the ability to animate `alpha`, `translation`, and `scale` values. It also supports additional functionality such as `followAnimatedTargetAlongPath` for more advanced motion. + +The controller is notified whenever children are added or removed from the layout, so that it can animate their entrance or exit, respectively. An example usage is Bubbles, which uses a PhysicsAnimationLayout for its stack of bubbles. Bubbles has controller subclasses including StackAnimationController and ExpansionAnimationController. StackAnimationController tells the layout to configure the translation animations to be chained (for the ‘following’ drag effect), and has methods such as ```moveStack(x, y)``` to animate the stack to a given point. ExpansionAnimationController asks for no animations to be chained, and exposes methods like ```expandStack()``` and ```collapseStack()```, which animate the bubbles to positions along the bottom of the screen. @@ -27,7 +31,7 @@ Returns a SpringForce instance to use for animations of the given property. This ### Animation Control Methods Once the layout has used the controller’s configuration properties to build the animations, the controller can use them to actually run animations. This is done for two reasons - reacting to a view being added or removed, or responding to another class (such as a touch handler or broadcast receiver) requesting an animation. ```onChildAdded```, ```onChildRemoved```, and ```setChildVisibility``` are called automatically by the layout, giving the controller the opportunity to animate the child in/out/visible/gone. Custom methods are called by anyone with access to the controller instance to do things like expand, collapse, or move the child views. -In either case, the controller can use `super.animationForChild` to retrieve a `PhysicsPropertyAnimator` instance. This object behaves similarly to the `ViewPropertyAnimator` object you would receive from `View.animate()`. +In either case, the controller can use `super.animationForChild` to retrieve a `PhysicsPropertyAnimator` instance. This object behaves similarly to the `ViewPropertyAnimator` object you would receive from `View.animate()`. #### PhysicsPropertyAnimator @@ -36,9 +40,14 @@ Like `ViewPropertyAnimator`, `PhysicsPropertyAnimator` provides the following me - `translationX/Y/Z(float)` - `scaleX/Y(float)` +...as well as shortcut methods to reduce the amount of boilerplate code needed for common use cases: +- `position(float, float, Runnable…)`, which starts translationX and translationY animations, and calls the provided callbacks only when both animations have completed. +- `followAnimatedTargetAlongPath(Path, int, TimeInterpolator)`, which animates a ‘target’ point along the given path using a traditional Animator. As the target moves, the translationX/Y physics animations are updated to follow the target, similarly to how they might follow a touch event location. This results in the view roughly following the path, but with natural motion that takes momentum into account. For example, if a path makes a 90 degree turn to the right, the physics animations will cause the view to curve naturally towards the new trajectory. + It also provides the following configuration methods: - `withStartDelay(int)`, for starting the animation after a given delay. - `withStartVelocity(float)`, for starting the animation with the given start velocity. +- `withStiffness(float)` and `withDampingRatio(float)`, for overriding the default physics param values returned by the controller’s getSpringForce method. - `withPositionStartVelocities(float, float)`, for setting specific start velocities for TRANSLATION_X and TRANSLATION_Y, since these typically differ. - `start(Runnable)`, to start the animation, with an optional end action to call when the animations for every property (including chained animations) have completed. @@ -61,8 +70,7 @@ The animator has additional functionality to reduce the amount of boilerplate re - Often, animations will set starting values for properties before the animation begins. Property methods like `translationX` have an overloaded variant: `translationX(from, to)`. When `start()` is called, the animation will set the view's translationX property to `from` before beginning the animation to `to`. - We may want to use different end actions for each property. For example, if we're animating a view to the bottom of the screen, and also fading it out, we might want to perform an action as soon as the fade out is complete. We can use `alpha(to, endAction)`, which will call endAction as soon as the alpha animation is finished. A special case is `position(x, y, endAction)`, where the endAction is called when both translationX and translationY animations have completed. - -`PhysicsAnimationController` also provides `animationsForChildrenFromIndex(int, ChildAnimationConfigurator)`. This is a convenience method for starting animations on multiple child views, starting at the given index. The `ChildAnimationConfigurator` is called with a `PhysicsPropertyAnimator` for each child, where calls to methods like `translationX` and `withStartVelocity` can be made. `animationsForChildrenFromIndex` returns a `MultiAnimationStarter` with a single method, `startAll(endAction)`, which starts all of the animations and calls the end action when they have all completed. +- `PhysicsAnimationController` also provides `animationsForChildrenFromIndex(int, ChildAnimationConfigurator)`. This is a convenience method for starting animations on multiple child views, starting at the given index. The `ChildAnimationConfigurator` is called with a `PhysicsPropertyAnimator` for each child, where calls to methods like `translationX` and `withStartVelocity` can be made. `animationsForChildrenFromIndex` returns a `MultiAnimationStarter` with a single method, `startAll(endAction)`, which starts all of the animations and calls the end action when they have all completed. ##### Examples Spring the stack of bubbles (whose animations are chained) to the bottom of the screen, shrinking them to 50% size. Once the first bubble is done shrinking, begin fading them out, and then remove them all from the parent once all bubbles have faded out: @@ -93,16 +101,22 @@ animationsForChildrenFromIndex(1, (index, anim) -> anim.translationX((index - 1) .startAll(removeFirstView); ``` -## PhysicsAnimationLayout -The layout itself is a FrameLayout descendant with a few extra methods: +Move a view up along the left side of the screen, and then to the top right of the screen (assume a view that is currently halfway down the left side of the screen). When the translation animations have finished following the target, call a callback: -```setController(PhysicsAnimationController controller)``` -Attaches the layout to the controller, so that the controller can access the layout’s protected methods. It also constructs or reconfigures the physics animations according to the new controller’s configuration methods. +``` +Path path = new Path(); +path.moveTo(view.getTranslationX(), view.getTranslationY()); +path.lineTo(view.getTranslationX(), 0); +path.lineTo(mScreenWidth, 0); +animationForChild(view) + .followAnimatedTargetAlongPath(path, 100, new LinearInterpolator()) + .start(callbackAfterFollowingFinished); +``` -```setEndListenerForProperty(ViewProperty property, AnimationEndListener endListener)``` -Sets an end listener that is called when all animations on the given property have ended. +## PhysicsAnimationLayout +The layout itself is a FrameLayout descendant with a few extra methods: -```setMaxRenderedChildren(int max)``` -Child views beyond this limit will be set to GONE, and won't be animated, for performance reasons. Defaults to **5**. +```setActiveController(PhysicsAnimationController controller)``` +Sets the given controller as the active controller for the layout. This causes the layout to construct or reconfigure the physics animations according to the new controller’s configuration methods, and halt any in-progress animations. -It has one protected method, ```animateValueForChildAtIndex(ViewProperty property, int index, float value)```, which is visible to PhysicsAnimationController descendants. This method dispatches the given value to the appropriate animation.
\ No newline at end of file +Only the currently active controller is allowed to start animations. If a different controller is set as the active controller, the previous controller will no longer be able to start animations. Attempts to do so will have no effect. This is to ensure that multiple controllers aren’t updating animations at the same time, which can cause undefined behavior.
\ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/values-cs/strings.xml b/packages/SystemUI/res-keyguard/values-cs/strings.xml index 3adc2792ef10..0d88a2074efa 100644 --- a/packages/SystemUI/res-keyguard/values-cs/strings.xml +++ b/packages/SystemUI/res-keyguard/values-cs/strings.xml @@ -41,7 +41,7 @@ <string name="keyguard_low_battery" msgid="9218432555787624490">"Připojte dobíjecí zařízení."</string> <string name="keyguard_instructions_when_pattern_disabled" msgid="8566679946700751371">"Klávesy odemknete stisknutím tlačítka nabídky."</string> <string name="keyguard_network_locked_message" msgid="6743537524631420759">"Síť je blokována"</string> - <string name="keyguard_missing_sim_message_short" msgid="6327533369959764518">"Chybí SIM karta"</string> + <string name="keyguard_missing_sim_message_short" msgid="6327533369959764518">"Není vložena SIM karta"</string> <string name="keyguard_missing_sim_message" product="tablet" msgid="4550152848200783542">"V tabletu není SIM karta."</string> <string name="keyguard_missing_sim_message" product="default" msgid="6585414237800161146">"V telefonu není SIM karta."</string> <string name="keyguard_missing_sim_instructions" msgid="7350295932015220392">"Vložte SIM kartu."</string> diff --git a/packages/SystemUI/res-keyguard/values-fa/strings.xml b/packages/SystemUI/res-keyguard/values-fa/strings.xml index 9c9de22b96d9..22c4c48a7e37 100644 --- a/packages/SystemUI/res-keyguard/values-fa/strings.xml +++ b/packages/SystemUI/res-keyguard/values-fa/strings.xml @@ -37,7 +37,7 @@ <string name="keyguard_plugged_in_wireless" msgid="8404159927155454732">"<xliff:g id="PERCENTAGE">%s</xliff:g> • درحال شارژ بیسیم"</string> <string name="keyguard_plugged_in" msgid="3161102098900158923">"<xliff:g id="PERCENTAGE">%s</xliff:g> • درحال شارژ شدن"</string> <string name="keyguard_plugged_in_charging_fast" msgid="3684592786276709342">"<xliff:g id="PERCENTAGE">%s</xliff:g> • درحال شارژ سریع"</string> - <string name="keyguard_plugged_in_charging_slowly" msgid="509533586841478405">"<xliff:g id="PERCENTAGE">%s</xliff:g> • آهستهآهسته شارژ میشود"</string> + <string name="keyguard_plugged_in_charging_slowly" msgid="509533586841478405">"<xliff:g id="PERCENTAGE">%s</xliff:g> • درحال شارژ آهسته"</string> <string name="keyguard_low_battery" msgid="9218432555787624490">"شارژر را وصل کنید."</string> <string name="keyguard_instructions_when_pattern_disabled" msgid="8566679946700751371">"برای باز کردن قفل روی «منو» فشار دهید."</string> <string name="keyguard_network_locked_message" msgid="6743537524631420759">"شبکه قفل شد"</string> diff --git a/packages/SystemUI/res/drawable/bubble_dismiss_circle.xml b/packages/SystemUI/res/drawable/bubble_dismiss_circle.xml index 1661bb22d148..8c7e82f82186 100644 --- a/packages/SystemUI/res/drawable/bubble_dismiss_circle.xml +++ b/packages/SystemUI/res/drawable/bubble_dismiss_circle.xml @@ -24,4 +24,5 @@ android:width="1dp" android:color="#66FFFFFF" /> + <solid android:color="#B3000000" /> </shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/bubble_dismiss_target.xml b/packages/SystemUI/res/layout/bubble_dismiss_target.xml index 245177c8461b..ca085b69c35d 100644 --- a/packages/SystemUI/res/layout/bubble_dismiss_target.xml +++ b/packages/SystemUI/res/layout/bubble_dismiss_target.xml @@ -20,6 +20,13 @@ android:layout_height="@dimen/pip_dismiss_gradient_height" android:layout_gravity="bottom|center_horizontal"> + <FrameLayout + android:id="@+id/bubble_dismiss_circle" + android:layout_width="@dimen/bubble_dismiss_encircle_size" + android:layout_height="@dimen/bubble_dismiss_encircle_size" + android:layout_gravity="center" + android:background="@drawable/bubble_dismiss_circle" /> + <LinearLayout android:id="@+id/bubble_dismiss_icon_container" android:layout_width="wrap_content" @@ -38,29 +45,5 @@ android:layout_width="24dp" android:layout_height="24dp" android:src="@drawable/bubble_dismiss_icon" /> - - <TextView - android:id="@+id/bubble_dismiss_text" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="9dp" - android:layout_marginBottom="9dp" - android:layout_marginLeft="8dp" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body1" - android:textColor="@android:color/white" - android:shadowColor="@android:color/black" - android:shadowDx="-1" - android:shadowDy="1" - android:shadowRadius="0.01" - android:text="@string/bubble_dismiss_text" /> - </LinearLayout> - - <FrameLayout - android:id="@+id/bubble_dismiss_circle" - android:layout_width="@dimen/bubble_dismiss_encircle_size" - android:layout_height="@dimen/bubble_dismiss_encircle_size" - android:layout_gravity="center" - android:alpha="0" - android:background="@drawable/bubble_dismiss_circle" /> </FrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/bubble_view.xml b/packages/SystemUI/res/layout/bubble_view.xml index a8eb2914b0b2..e2dea45e3406 100644 --- a/packages/SystemUI/res/layout/bubble_view.xml +++ b/packages/SystemUI/res/layout/bubble_view.xml @@ -24,7 +24,6 @@ android:id="@+id/bubble_image" android:layout_width="@dimen/individual_bubble_size" android:layout_height="@dimen/individual_bubble_size" - android:padding="@dimen/bubble_view_padding" android:clipToPadding="false"/> </com.android.systemui.bubbles.BubbleView> diff --git a/packages/SystemUI/res/layout/super_status_bar.xml b/packages/SystemUI/res/layout/super_status_bar.xml index a91493003bb5..9716a00a7f72 100644 --- a/packages/SystemUI/res/layout/super_status_bar.xml +++ b/packages/SystemUI/res/layout/super_status_bar.xml @@ -44,7 +44,7 @@ </com.android.systemui.statusbar.BackDropView> <com.android.systemui.statusbar.ScrimView - android:id="@+id/scrim_behind" + android:id="@+id/scrim_for_bubble" android:layout_width="match_parent" android:layout_height="match_parent" android:importantForAccessibility="no" @@ -56,6 +56,14 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> + <com.android.systemui.statusbar.ScrimView + android:id="@+id/scrim_behind" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:importantForAccessibility="no" + sysui:ignoreRightInset="true" + /> + <include layout="@layout/status_bar_expanded" android:layout_width="match_parent" android:layout_height="match_parent" diff --git a/packages/SystemUI/res/values-be/strings.xml b/packages/SystemUI/res/values-be/strings.xml index 36745648b051..dcd31bda79b3 100644 --- a/packages/SystemUI/res/values-be/strings.xml +++ b/packages/SystemUI/res/values-be/strings.xml @@ -329,7 +329,7 @@ <string name="quick_settings_location_off_label" msgid="7464544086507331459">"Вызначэнне месцазнаходжання адключана"</string> <string name="quick_settings_media_device_label" msgid="1302906836372603762">"Мультымедыйная прылада"</string> <string name="quick_settings_rssi_label" msgid="7725671335550695589">"RSSI"</string> - <string name="quick_settings_rssi_emergency_only" msgid="2713774041672886750">"Толькі экстранныя выклікі"</string> + <string name="quick_settings_rssi_emergency_only" msgid="2713774041672886750">"Толькі экстраныя выклікі"</string> <string name="quick_settings_settings_label" msgid="5326556592578065401">"Налады"</string> <string name="quick_settings_time_label" msgid="4635969182239736408">"Час"</string> <string name="quick_settings_user_label" msgid="5238995632130897840">"Я"</string> @@ -423,7 +423,7 @@ <string name="keyguard_indication_charging_time_wireless" msgid="6959284458466962592">"<xliff:g id="PERCENTAGE">%2$s</xliff:g> • Ідзе бесправадная зарадка (да поўнага зараду засталося <xliff:g id="CHARGING_TIME_LEFT">%1$s</xliff:g>)"</string> <string name="keyguard_indication_charging_time" msgid="2056340799276374421">"Ідзе зарадка (<xliff:g id="PERCENTAGE">%2$s</xliff:g>, яшчэ <xliff:g id="CHARGING_TIME_LEFT">%1$s</xliff:g>)"</string> <string name="keyguard_indication_charging_time_fast" msgid="7767562163577492332">"Ідзе хуткая зарадка (<xliff:g id="PERCENTAGE">%2$s</xliff:g>, яшчэ <xliff:g id="CHARGING_TIME_LEFT">%1$s</xliff:g>)"</string> - <string name="keyguard_indication_charging_time_slowly" msgid="3769655133567307069">"Ідзе павольная зарадка (<xliff:g id="PERCENTAGE">%2$s</xliff:g>, <xliff:g id="CHARGING_TIME_LEFT">%1$s</xliff:g> да канца)"</string> + <string name="keyguard_indication_charging_time_slowly" msgid="3769655133567307069">"Ідзе павольная зарадка (<xliff:g id="PERCENTAGE">%2$s</xliff:g>, яшчэ <xliff:g id="CHARGING_TIME_LEFT">%1$s</xliff:g>)"</string> <string name="accessibility_multi_user_switch_switcher" msgid="7305948938141024937">"Перайсці да іншага карыстальніка"</string> <string name="accessibility_multi_user_switch_switcher_with_current" msgid="8434880595284601601">"Перайсці да іншага карыстальніка, бягучы карыстальнік <xliff:g id="CURRENT_USER_NAME">%s</xliff:g>"</string> <string name="accessibility_multi_user_switch_inactive" msgid="1424081831468083402">"Бягучы карыстальнік <xliff:g id="CURRENT_USER_NAME">%s</xliff:g>"</string> diff --git a/packages/SystemUI/res/values-bn/strings.xml b/packages/SystemUI/res/values-bn/strings.xml index 6b7561bdcd3b..5141b57ec295 100644 --- a/packages/SystemUI/res/values-bn/strings.xml +++ b/packages/SystemUI/res/values-bn/strings.xml @@ -232,9 +232,9 @@ <string name="accessibility_quick_settings_airplane_changed_on" msgid="8983005603505087728">"বিমান মোড চালু হয়েছে।"</string> <string name="accessibility_quick_settings_dnd_none_on" msgid="2960643943620637020">"সম্পূর্ণ নীরব"</string> <string name="accessibility_quick_settings_dnd_alarms_on" msgid="3357131899365865386">"শুধুমাত্র অ্যালার্ম"</string> - <string name="accessibility_quick_settings_dnd" msgid="5555155552520665891">"বিরক্ত করবে না।"</string> - <string name="accessibility_quick_settings_dnd_changed_off" msgid="2757071272328547807">"\'বিরক্ত করবে না\' বন্ধ আছে।"</string> - <string name="accessibility_quick_settings_dnd_changed_on" msgid="6808220653747701059">"\'বিরক্ত করবে না\' চালু করা হয়েছে।"</string> + <string name="accessibility_quick_settings_dnd" msgid="5555155552520665891">"বিরক্ত করবেন না।"</string> + <string name="accessibility_quick_settings_dnd_changed_off" msgid="2757071272328547807">"\'বিরক্ত করবেন না\' বন্ধ আছে।"</string> + <string name="accessibility_quick_settings_dnd_changed_on" msgid="6808220653747701059">"\'বিরক্ত করবেন না\' চালু করা হয়েছে।"</string> <string name="accessibility_quick_settings_bluetooth" msgid="6341675755803320038">"ব্লুটুথ"</string> <string name="accessibility_quick_settings_bluetooth_off" msgid="2133631372372064339">"ব্লুটুথ বন্ধ আছে।"</string> <string name="accessibility_quick_settings_bluetooth_on" msgid="7681999166216621838">"ব্লুটুথ চালু আছে।"</string> @@ -299,7 +299,7 @@ <string name="start_dreams" msgid="5640361424498338327">"স্ক্রিন সেভার"</string> <string name="ethernet_label" msgid="7967563676324087464">"ইথারনেট"</string> <string name="quick_settings_header_onboarding_text" msgid="8030309023792936283">"আরও বিকল্পের জন্য আইকনগুলি টাচ করে ধরে থাকুন"</string> - <string name="quick_settings_dnd_label" msgid="7112342227663678739">"বিরক্ত করবে না"</string> + <string name="quick_settings_dnd_label" msgid="7112342227663678739">"বিরক্ত করবেন না"</string> <string name="quick_settings_dnd_priority_label" msgid="483232950670692036">"শুধুমাত্র অগ্রাধিকার"</string> <string name="quick_settings_dnd_alarms_label" msgid="2559229444312445858">"শুধুমাত্র অ্যালার্মগুলি"</string> <string name="quick_settings_dnd_none_label" msgid="5025477807123029478">"একদম নিরব"</string> @@ -461,7 +461,7 @@ <string name="manage_notifications_text" msgid="2386728145475108753">"পরিচালনা করুন"</string> <string name="notification_section_header_gentle" msgid="4372438504154095677">"নীরব বিজ্ঞপ্তি"</string> <string name="accessibility_notification_section_header_gentle_clear_all" msgid="4286716295850400959">"সব নীরব বিজ্ঞপ্তি মুছুন"</string> - <string name="dnd_suppressing_shade_text" msgid="1904574852846769301">"\'বিরক্ত করবে না\' দিয়ে বিজ্ঞপ্তি পজ করা হয়েছে"</string> + <string name="dnd_suppressing_shade_text" msgid="1904574852846769301">"\'বিরক্ত করবেন না\' দিয়ে বিজ্ঞপ্তি পজ করা হয়েছে"</string> <string name="media_projection_action_text" msgid="8470872969457985954">"এখন শুরু করুন"</string> <string name="empty_shade_text" msgid="708135716272867002">"কোনো বিজ্ঞপ্তি নেই"</string> <string name="profile_owned_footer" msgid="8021888108553696069">"প্রোফাইল পর্যবেক্ষণ করা হতে পারে"</string> @@ -737,9 +737,9 @@ <string name="keyboard_shortcut_group_applications_youtube" msgid="6555453761294723317">"YouTube"</string> <string name="keyboard_shortcut_group_applications_calendar" msgid="9043614299194991263">"ক্যালেন্ডার"</string> <string name="tuner_full_zen_title" msgid="4540823317772234308">"ভলিউম নিয়ন্ত্রণ সহ দেখান"</string> - <string name="volume_and_do_not_disturb" msgid="1750270820297253561">"বিরক্ত করবে না"</string> + <string name="volume_and_do_not_disturb" msgid="1750270820297253561">"বিরক্ত করবেন না"</string> <string name="volume_dnd_silent" msgid="4363882330723050727">"ভলিউম বোতামের শর্টকাট"</string> - <string name="volume_up_silent" msgid="7545869833038212815">"ভলিউম বাড়িয়ে \'বিরক্ত করবে না\' মোড থেকে বেরিয়ে আসুন"</string> + <string name="volume_up_silent" msgid="7545869833038212815">"ভলিউম বাড়িয়ে \'বিরক্ত করবেন না\' মোড থেকে বেরিয়ে আসুন"</string> <string name="battery" msgid="7498329822413202973">"ব্যাটারি"</string> <string name="clock" msgid="7416090374234785905">"ঘড়ি"</string> <string name="headset" msgid="4534219457597457353">"হেডসেট"</string> @@ -883,10 +883,10 @@ <string name="mobile_carrier_text_format" msgid="3241721038678469804">"<xliff:g id="CARRIER_NAME">%1$s</xliff:g>, <xliff:g id="MOBILE_DATA_TYPE">%2$s</xliff:g>"</string> <string name="wifi_is_off" msgid="1838559392210456893">"ওয়াই ফাই বন্ধ আছে"</string> <string name="bt_is_off" msgid="2640685272289706392">"ব্লুটুথ বন্ধ আছে"</string> - <string name="dnd_is_off" msgid="6167780215212497572">"বিরক্ত করবে না বিকল্পটি বন্ধ আছে"</string> - <string name="qs_dnd_prompt_auto_rule" msgid="862559028345233052">"বিরক্ত করবে না বিকল্পটি একটি স্বয়ংক্রিয় নিয়ম <xliff:g id="ID_1">%s</xliff:g> এর দ্বারা চালু করা হয়েছে।"</string> - <string name="qs_dnd_prompt_app" msgid="7978037419334156034">"বিরক্ত করবে না বিকল্পটি একটি অ্যাপ <xliff:g id="ID_1">%s</xliff:g> এর দ্বারা চালু করা হয়েছে।"</string> - <string name="qs_dnd_prompt_auto_rule_app" msgid="2599343675391111951">"বিরক্ত করবে না বিকল্পটি একটি স্বয়ংক্রিয় নিয়ম বা অ্যাপের দ্বারা চালু করা হয়েছে।"</string> + <string name="dnd_is_off" msgid="6167780215212497572">"বিরক্ত করবেন না বিকল্পটি বন্ধ আছে"</string> + <string name="qs_dnd_prompt_auto_rule" msgid="862559028345233052">"বিরক্ত করবেন না বিকল্পটি একটি স্বয়ংক্রিয় নিয়ম <xliff:g id="ID_1">%s</xliff:g> এর দ্বারা চালু করা হয়েছে।"</string> + <string name="qs_dnd_prompt_app" msgid="7978037419334156034">"বিরক্ত করবেন না বিকল্পটি একটি অ্যাপ <xliff:g id="ID_1">%s</xliff:g> এর দ্বারা চালু করা হয়েছে।"</string> + <string name="qs_dnd_prompt_auto_rule_app" msgid="2599343675391111951">"বিরক্ত করবেন না বিকল্পটি একটি স্বয়ংক্রিয় নিয়ম বা অ্যাপের দ্বারা চালু করা হয়েছে।"</string> <string name="qs_dnd_until" msgid="3469471136280079874">"<xliff:g id="ID_1">%s</xliff:g> পর্যন্ত"</string> <string name="qs_dnd_keep" msgid="1825009164681928736">"রাখুন"</string> <string name="qs_dnd_replace" msgid="8019520786644276623">"বদলে দিন"</string> diff --git a/packages/SystemUI/res/values-cs/strings.xml b/packages/SystemUI/res/values-cs/strings.xml index 87aaa94d1dd3..abea52a0adf8 100644 --- a/packages/SystemUI/res/values-cs/strings.xml +++ b/packages/SystemUI/res/values-cs/strings.xml @@ -194,7 +194,7 @@ <string name="accessibility_bluetooth_tether" msgid="4102784498140271969">"Sdílené připojení přes Bluetooth."</string> <string name="accessibility_airplane_mode" msgid="834748999790763092">"Režim Letadlo."</string> <string name="accessibility_vpn_on" msgid="5993385083262856059">"VPN je zapnuto."</string> - <string name="accessibility_no_sims" msgid="3957997018324995781">"Chybí SIM karta"</string> + <string name="accessibility_no_sims" msgid="3957997018324995781">"Není vložena SIM karta"</string> <string name="carrier_network_change_mode" msgid="8149202439957837762">"Probíhá změna sítě operátora"</string> <string name="accessibility_battery_details" msgid="7645516654955025422">"Otevřít podrobnosti o baterii"</string> <string name="accessibility_battery_level" msgid="7451474187113371965">"Stav baterie: <xliff:g id="NUMBER">%d</xliff:g> procent."</string> diff --git a/packages/SystemUI/res/values-fr-rCA/strings.xml b/packages/SystemUI/res/values-fr-rCA/strings.xml index e8e4232063e3..137b780fee2a 100644 --- a/packages/SystemUI/res/values-fr-rCA/strings.xml +++ b/packages/SystemUI/res/values-fr-rCA/strings.xml @@ -417,7 +417,7 @@ <string name="keyguard_indication_charging_time_wireless" msgid="6959284458466962592">"<xliff:g id="PERCENTAGE">%2$s</xliff:g> • En recharge sans fil (<xliff:g id="CHARGING_TIME_LEFT">%1$s</xliff:g> jusqu\'à la recharge complète)"</string> <string name="keyguard_indication_charging_time" msgid="2056340799276374421">"En recharge : <xliff:g id="PERCENTAGE">%2$s</xliff:g> (<xliff:g id="CHARGING_TIME_LEFT">%1$s</xliff:g> jusqu\'à charge complète)"</string> <string name="keyguard_indication_charging_time_fast" msgid="7767562163577492332">"En recharge rapide : <xliff:g id="PERCENTAGE">%2$s</xliff:g> (<xliff:g id="CHARGING_TIME_LEFT">%1$s</xliff:g> jusqu\'à ch. comp.)"</string> - <string name="keyguard_indication_charging_time_slowly" msgid="3769655133567307069">"Recharge lente : <xliff:g id="PERCENTAGE">%2$s</xliff:g> (à 100 %% dans <xliff:g id="CHARGING_TIME_LEFT">%1$s</xliff:g>)"</string> + <string name="keyguard_indication_charging_time_slowly" msgid="3769655133567307069">"En recharge lente : <xliff:g id="PERCENTAGE">%2$s</xliff:g> (<xliff:g id="CHARGING_TIME_LEFT">%1$s</xliff:g> jusqu\'à ch. comp.)"</string> <string name="accessibility_multi_user_switch_switcher" msgid="7305948938141024937">"Changer d\'utilisateur"</string> <string name="accessibility_multi_user_switch_switcher_with_current" msgid="8434880595284601601">"Changer d\'utilisateur (utilisateur actuel <xliff:g id="CURRENT_USER_NAME">%s</xliff:g>)"</string> <string name="accessibility_multi_user_switch_inactive" msgid="1424081831468083402">"Utilisateur actuel : <xliff:g id="CURRENT_USER_NAME">%s</xliff:g>"</string> diff --git a/packages/SystemUI/res/values-gu/strings.xml b/packages/SystemUI/res/values-gu/strings.xml index 1a15cf705d91..f90f494fe3a2 100644 --- a/packages/SystemUI/res/values-gu/strings.xml +++ b/packages/SystemUI/res/values-gu/strings.xml @@ -314,7 +314,7 @@ <string name="quick_settings_bluetooth_secondary_label_hearing_aids" msgid="4930931771490695395">"શ્રવણ યંત્રો"</string> <string name="quick_settings_bluetooth_secondary_label_transient" msgid="4551281899312150640">"ચાલુ કરી રહ્યાં છીએ…"</string> <string name="quick_settings_brightness_label" msgid="6968372297018755815">"તેજ"</string> - <string name="quick_settings_rotation_unlocked_label" msgid="7305323031808150099">"ઑટો રોટેટ"</string> + <string name="quick_settings_rotation_unlocked_label" msgid="7305323031808150099">"આપમેળે ફેરવો"</string> <string name="accessibility_quick_settings_rotation" msgid="4231661040698488779">"સ્ક્રીનને આપમેળે ફેરવો"</string> <string name="accessibility_quick_settings_rotation_value" msgid="8187398200140760213">"<xliff:g id="ID_1">%s</xliff:g> મોડ"</string> <string name="quick_settings_rotation_locked_label" msgid="6359205706154282377">"પરિભ્રમણ લૉક થયું"</string> diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml index 90e78e85ed19..5a8c5dc68bdf 100644 --- a/packages/SystemUI/res/values-land/dimens.xml +++ b/packages/SystemUI/res/values-land/dimens.xml @@ -36,4 +36,8 @@ <dimen name="volume_tool_tip_right_margin">136dp</dimen> <dimen name="volume_tool_tip_top_margin">12dp</dimen> + + <!-- Padding between status bar and bubbles when displayed in expanded state, smaller + value in landscape since we have limited vertical space--> + <dimen name="bubble_padding_top">4dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-or/strings.xml b/packages/SystemUI/res/values-or/strings.xml index e9b150c1a66e..3760007b2291 100644 --- a/packages/SystemUI/res/values-or/strings.xml +++ b/packages/SystemUI/res/values-or/strings.xml @@ -250,9 +250,9 @@ <string name="accessibility_quick_settings_close" msgid="3115847794692516306">"ପ୍ୟାନେଲ୍ ବନ୍ଦ କରନ୍ତୁ।"</string> <string name="accessibility_quick_settings_more_time" msgid="3659274935356197708">"ଅଧିକ ସମୟ।"</string> <string name="accessibility_quick_settings_less_time" msgid="2404728746293515623">"କମ୍ ସମୟ।"</string> - <string name="accessibility_quick_settings_flashlight_off" msgid="4936432000069786988">"ଫ୍ଲାସ୍ଲାଇଟ୍ ବନ୍ଦ ଅଛି।"</string> + <string name="accessibility_quick_settings_flashlight_off" msgid="4936432000069786988">"ଫ୍ଲାଶ୍ଲାଇଟ୍ ଅଫ୍ ଅଛି।"</string> <string name="accessibility_quick_settings_flashlight_unavailable" msgid="8012811023312280810">"ଟର୍ଚ୍ଚ ଲାଇଟ୍ ଅନୁପଲବ୍ଧ।"</string> - <string name="accessibility_quick_settings_flashlight_on" msgid="2003479320007841077">"ଫ୍ଲାସ୍ଲାଇଟ୍ ଚାଲୁଅଛି।"</string> + <string name="accessibility_quick_settings_flashlight_on" msgid="2003479320007841077">"ଫ୍ଲାଶ୍ଲାଇଟ୍ ଅନ୍ ଅଛି।"</string> <string name="accessibility_quick_settings_flashlight_changed_off" msgid="3303701786768224304">"ଟର୍ଚ୍ଚ ଲାଇଟ୍ ବନ୍ଦ ଅଛି।"</string> <string name="accessibility_quick_settings_flashlight_changed_on" msgid="6531793301533894686">"ଟର୍ଚ୍ଚ ଲାଇଟ୍ ଅନ୍ ଅଛି।"</string> <string name="accessibility_quick_settings_color_inversion_changed_off" msgid="4406577213290173911">"ରଙ୍ଗ ବିପରୀତିକରଣକୁ ବନ୍ଦ କରିଦିଆଗଲା।"</string> @@ -362,7 +362,7 @@ <item quantity="one">%d ଡିଭାଇସ୍</item> </plurals> <string name="quick_settings_notifications_label" msgid="4818156442169154523">"ବିଜ୍ଞପ୍ତି"</string> - <string name="quick_settings_flashlight_label" msgid="2133093497691661546">"ଫ୍ଲାସ୍ଲାଇଟ୍"</string> + <string name="quick_settings_flashlight_label" msgid="2133093497691661546">"ଫ୍ଲାଶ୍ଲାଇଟ"</string> <string name="quick_settings_cellular_detail_title" msgid="3661194685666477347">"ମୋବାଇଲ୍ ଡାଟା"</string> <string name="quick_settings_cellular_detail_data_usage" msgid="1964260360259312002">"ଡାଟାର ବ୍ୟବହାର"</string> <string name="quick_settings_cellular_detail_remaining_data" msgid="722715415543541249">"ଅବଶିଷ୍ଟ ଡାଟା"</string> diff --git a/packages/SystemUI/res/values-vi/strings.xml b/packages/SystemUI/res/values-vi/strings.xml index d3caa2b2c75d..6f38b18693d3 100644 --- a/packages/SystemUI/res/values-vi/strings.xml +++ b/packages/SystemUI/res/values-vi/strings.xml @@ -55,11 +55,11 @@ <string name="label_view" msgid="6304565553218192990">"Xem"</string> <string name="always_use_device" msgid="4015357883336738417">"Luôn mở <xliff:g id="APPLICATION">%1$s</xliff:g> khi kết nối <xliff:g id="USB_DEVICE">%2$s</xliff:g>"</string> <string name="always_use_accessory" msgid="3257892669444535154">"Luôn mở <xliff:g id="APPLICATION">%1$s</xliff:g> khi kết nối <xliff:g id="USB_ACCESSORY">%2$s</xliff:g>"</string> - <string name="usb_debugging_title" msgid="4513918393387141949">"Cho phép gỡ lỗi qua USB?"</string> + <string name="usb_debugging_title" msgid="4513918393387141949">"Cho phép gỡ lỗi USB?"</string> <string name="usb_debugging_message" msgid="2220143855912376496">"Tệp tham chiếu khóa RSA của máy tính là:\n<xliff:g id="FINGERPRINT">%1$s</xliff:g>"</string> <string name="usb_debugging_always" msgid="303335496705863070">"Luôn cho phép từ máy tính này"</string> <string name="usb_debugging_allow" msgid="2272145052073254852">"Cho phép"</string> - <string name="usb_debugging_secondary_user_title" msgid="6353808721761220421">"Không cho phép chế độ gỡ lỗi qua USB"</string> + <string name="usb_debugging_secondary_user_title" msgid="6353808721761220421">"Tính năng gỡ lỗi USB không được phép"</string> <string name="usb_debugging_secondary_user_message" msgid="6067122453571699801">"Người dùng hiện đã đăng nhập vào thiết bị này không thể bật tính năng gỡ lỗi USB. Để sử dụng tính năng này, hãy chuyển sang người dùng chính."</string> <string name="usb_contaminant_title" msgid="206854874263058490">"Đã tắt cổng USB"</string> <string name="usb_contaminant_message" msgid="7379089091591609111">"Để bảo vệ thiết bị của bạn khỏi chất lỏng hay mảnh vỡ, cổng USB sẽ tắt và không phát hiện được bất kỳ phụ kiện nào.\n\nBạn sẽ nhận được thông báo khi có thể sử dụng lại cổng USB."</string> diff --git a/packages/SystemUI/res/values-zh-rTW/strings.xml b/packages/SystemUI/res/values-zh-rTW/strings.xml index 6bacf702cd64..03e31cdc7ee6 100644 --- a/packages/SystemUI/res/values-zh-rTW/strings.xml +++ b/packages/SystemUI/res/values-zh-rTW/strings.xml @@ -264,8 +264,8 @@ <string name="accessibility_quick_settings_work_mode_on" msgid="7650588553988014341">"工作模式已開啟。"</string> <string name="accessibility_quick_settings_work_mode_changed_off" msgid="5605534876107300711">"工作模式已關閉。"</string> <string name="accessibility_quick_settings_work_mode_changed_on" msgid="249840330756998612">"工作模式已開啟。"</string> - <string name="accessibility_quick_settings_data_saver_changed_off" msgid="650231949881093289">"數據節省模式已關閉。"</string> - <string name="accessibility_quick_settings_data_saver_changed_on" msgid="4218725402373934151">"數據節省模式已開啟。"</string> + <string name="accessibility_quick_settings_data_saver_changed_off" msgid="650231949881093289">"Data Saver 已關閉。"</string> + <string name="accessibility_quick_settings_data_saver_changed_on" msgid="4218725402373934151">"Data Saver 已開啟。"</string> <string name="accessibility_quick_settings_sensor_privacy_changed_off" msgid="5152819588955163090">"已關閉感應器隱私設定。"</string> <string name="accessibility_quick_settings_sensor_privacy_changed_on" msgid="529705259565826355">"已開啟感應器隱私設定。"</string> <string name="accessibility_brightness" msgid="8003681285547803095">"螢幕亮度"</string> @@ -747,8 +747,8 @@ <string name="accessibility_status_bar_headphones" msgid="9156307120060559989">"已與耳機連線"</string> <string name="accessibility_status_bar_headset" msgid="8666419213072449202">"已與耳機連線"</string> <string name="data_saver" msgid="5037565123367048522">"數據節省模式"</string> - <string name="accessibility_data_saver_on" msgid="8454111686783887148">"數據節省模式已開啟"</string> - <string name="accessibility_data_saver_off" msgid="8841582529453005337">"數據節省模式已關閉"</string> + <string name="accessibility_data_saver_on" msgid="8454111686783887148">"Data Saver 已開啟"</string> + <string name="accessibility_data_saver_off" msgid="8841582529453005337">"Data Saver 已關閉"</string> <string name="switch_bar_on" msgid="1142437840752794229">"開啟"</string> <string name="switch_bar_off" msgid="8803270596930432874">"關閉"</string> <string name="nav_bar" msgid="1993221402773877607">"導覽列"</string> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index be815e13e68e..2e1799168b5d 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1102,18 +1102,23 @@ <dimen name="bubble_flyout_pointer_size">6dp</dimen> <!-- How much space to leave between the flyout (tip of the arrow) and the bubble stack. --> <dimen name="bubble_flyout_space_from_bubble">8dp</dimen> - <!-- Padding around a collapsed bubble --> - <dimen name="bubble_view_padding">0dp</dimen> - <!-- Padding between bubbles when displayed in expanded state --> - <dimen name="bubble_padding">8dp</dimen> + <!-- Padding between status bar and bubbles when displayed in expanded state --> + <dimen name="bubble_padding_top">16dp</dimen> <!-- Size of individual bubbles. --> - <dimen name="individual_bubble_size">52dp</dimen> + <dimen name="individual_bubble_size">60dp</dimen> + <!-- Size of bubble icon bitmap. --> + <dimen name="bubble_icon_bitmap_size">52dp</dimen> + <!-- Extra padding added to the touchable rect for bubbles so they are easier to grab. --> + <dimen name="bubble_touch_padding">12dp</dimen> <!-- Size of the circle around the bubbles when they're in the dismiss target. --> - <dimen name="bubble_dismiss_encircle_size">56dp</dimen> + <dimen name="bubble_dismiss_encircle_size">52dp</dimen> <!-- How much to inset the icon in the circle --> <dimen name="bubble_icon_inset">16dp</dimen> <!-- Padding around the view displayed when the bubble is expanded --> <dimen name="bubble_expanded_view_padding">4dp</dimen> + <!-- This should be at least the size of bubble_expanded_view_padding; it is used to include + a slight touch slop around the expanded view. --> + <dimen name="bubble_expanded_view_slop">8dp</dimen> <!-- Default (and minimum) height of the expanded view shown when the bubble is expanded --> <dimen name="bubble_expanded_default_height">180dp</dimen> <!-- Height of the triangle that points to the expanded bubble --> @@ -1122,10 +1127,8 @@ <dimen name="bubble_pointer_width">6dp</dimen> <!-- Extra padding around the dismiss target for bubbles --> <dimen name="bubble_dismiss_slop">16dp</dimen> - <!-- Height of the header within the expanded view. --> - <dimen name="bubble_expanded_header_height">48dp</dimen> - <!-- Left and right padding applied to the header. --> - <dimen name="bubble_expanded_header_horizontal_padding">24dp</dimen> + <!-- Height of button allowing users to adjust settings for bubbles. --> + <dimen name="bubble_settings_size">48dp</dimen> <!-- How far, horizontally, to animate the expanded view over when animating in/out. --> <dimen name="bubble_expanded_animate_x_distance">100dp</dimen> <!-- How far, vertically, to animate the expanded view over when animating in/out. --> @@ -1138,12 +1141,10 @@ <dimen name="bubble_message_padding">4dp</dimen> <!-- Offset between bubbles in their stacked position. --> <dimen name="bubble_stack_offset">5dp</dimen> - <!-- How far offscreen the bubble stack rests. --> - <dimen name="bubble_stack_offscreen">5dp</dimen> + <!-- How far offscreen the bubble stack rests. Cuts off padding and part of icon bitmap. --> + <dimen name="bubble_stack_offscreen">9dp</dimen> <!-- How far down the screen the stack starts. --> - <dimen name="bubble_stack_starting_offset_y">100dp</dimen> - <!-- Size of image buttons in the bubble header --> - <dimen name="bubble_header_icon_size">48dp</dimen> + <dimen name="bubble_stack_starting_offset_y">96dp</dimen> <!-- Space between the pointer triangle and the bubble expanded view --> <dimen name="bubble_pointer_margin">8dp</dimen> <!-- Height of the permission prompt shown with bubbles --> @@ -1152,6 +1153,7 @@ snap to the dismiss target. --> <dimen name="bubble_dismiss_target_padding_x">40dp</dimen> <dimen name="bubble_dismiss_target_padding_y">20dp</dimen> + <!-- Size of the RAT type for CellularTile --> <dimen name="celltile_rat_type_size">10sp</dimen> </resources> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 66f19495dfa6..372718139886 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -137,6 +137,7 @@ <item type="id" name="scale_x_dynamicanimation_tag"/> <item type="id" name="scale_y_dynamicanimation_tag"/> <item type="id" name="physics_animator_tag"/> + <item type="id" name="target_animator_tag" /> <!-- Global Actions Menu --> <item type="id" name="global_actions_view" /> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java index 77571613f6af..5ddf89c08887 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java @@ -72,6 +72,13 @@ public abstract class TaskStackChangeListener { */ public void onSingleTaskDisplayDrawn(int displayId) { } + /** + * Called when the last task is removed from a display which can only contain one task. + * + * @param displayId the id of the display from which the window is removed. + */ + public void onSingleTaskDisplayEmpty(int displayId) {} + public void onTaskProfileLocked(int taskId, int userId) { } public void onTaskCreated(int taskId, ComponentName componentName) { } public void onTaskRemoved(int taskId) { } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java index a7f4396fabed..820057a168a0 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java @@ -209,6 +209,12 @@ public class TaskStackChangeListeners extends TaskStackListener { } @Override + public void onSingleTaskDisplayEmpty(int displayId) throws RemoteException { + mHandler.obtainMessage(H.ON_SINGLE_TASK_DISPLAY_EMPTY, displayId, + 0 /* unused */).sendToTarget(); + } + + @Override public void onTaskDisplayChanged(int taskId, int newDisplayId) throws RemoteException { mHandler.obtainMessage(H.ON_TASK_DISPLAY_CHANGED, taskId, newDisplayId).sendToTarget(); } @@ -240,6 +246,7 @@ public class TaskStackChangeListeners extends TaskStackListener { private static final int ON_SINGLE_TASK_DISPLAY_DRAWN = 19; private static final int ON_TASK_DISPLAY_CHANGED = 20; private static final int ON_TASK_LIST_UPDATED = 21; + private static final int ON_SINGLE_TASK_DISPLAY_EMPTY = 22; public H(Looper looper) { @@ -382,6 +389,13 @@ public class TaskStackChangeListeners extends TaskStackListener { } break; } + case ON_SINGLE_TASK_DISPLAY_EMPTY: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onSingleTaskDisplayEmpty( + msg.arg1); + } + break; + } case ON_TASK_DISPLAY_CHANGED: { for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { mTaskStackListeners.get(i).onTaskDisplayChanged(msg.arg1, msg.arg2); diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java index 39617ecd2770..45142b0bdc79 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java @@ -141,11 +141,12 @@ public class SystemUIFactory { } public ScrimController createScrimController(ScrimView scrimBehind, ScrimView scrimInFront, + ScrimView scrimForBubble, LockscreenWallpaper lockscreenWallpaper, TriConsumer<ScrimState, Float, GradientColors> scrimStateListener, Consumer<Integer> scrimVisibleListener, DozeParameters dozeParameters, AlarmManager alarmManager, KeyguardMonitor keyguardMonitor) { - return new ScrimController(scrimBehind, scrimInFront, scrimStateListener, + return new ScrimController(scrimBehind, scrimInFront, scrimForBubble, scrimStateListener, scrimVisibleListener, dozeParameters, alarmManager, keyguardMonitor); } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgeRenderer.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgeRenderer.java deleted file mode 100644 index 74ad0faca6d3..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BadgeRenderer.java +++ /dev/null @@ -1,94 +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.bubbles; - -import static android.graphics.Paint.ANTI_ALIAS_FLAG; -import static android.graphics.Paint.FILTER_BITMAP_FLAG; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Point; -import android.graphics.Rect; -import android.util.Log; - -import com.android.systemui.R; - -// XXX: Mostly opied from launcher code / can we share? -/** - * Contains parameters necessary to draw a badge for an icon (e.g. the size of the badge). - */ -public class BadgeRenderer { - - private static final String TAG = "BadgeRenderer"; - - /** The badge sizes are defined as percentages of the app icon size. */ - private static final float SIZE_PERCENTAGE = 0.38f; - - /** Extra scale down of the dot. */ - private static final float DOT_SCALE = 0.6f; - - private final float mDotCenterOffset; - private final float mCircleRadius; - private final Paint mCirclePaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG); - - public BadgeRenderer(Context context) { - mDotCenterOffset = getDotCenterOffset(context); - mCircleRadius = getDotRadius(mDotCenterOffset); - } - - /** Space between the center of the dot and the top or left of the bubble stack. */ - static float getDotCenterOffset(Context context) { - final int iconSizePx = - context.getResources().getDimensionPixelSize(R.dimen.individual_bubble_size); - return SIZE_PERCENTAGE * iconSizePx; - } - - static float getDotRadius(float dotCenterOffset) { - int size = (int) (DOT_SCALE * dotCenterOffset); - return size / 2f; - } - - /** - * Draw a circle in the top right corner of the given bounds. - * - * @param color The color (based on the icon) to use for the badge. - * @param iconBounds The bounds of the icon being badged. - * @param badgeScale The progress of the animation, from 0 to 1. - * @param spaceForOffset How much space to offset the badge up and to the left or right. - * @param onLeft Whether the badge should be draw on left or right side. - */ - public void draw(Canvas canvas, int color, Rect iconBounds, float badgeScale, - Point spaceForOffset, boolean onLeft) { - if (iconBounds == null) { - Log.e(TAG, "Invalid null argument(s) passed in call to draw."); - return; - } - canvas.save(); - // We draw the badge relative to its center. - int x = onLeft ? iconBounds.left : iconBounds.right; - float offset = onLeft ? (mDotCenterOffset / 2) : -(mDotCenterOffset / 2); - float badgeCenterX = x + offset; - float badgeCenterY = iconBounds.top + mDotCenterOffset / 2; - - canvas.translate(badgeCenterX + spaceForOffset.x, badgeCenterY - spaceForOffset.y); - - canvas.scale(badgeScale, badgeScale); - mCirclePaint.setColor(color); - canvas.drawCircle(0, 0, mCircleRadius, mCirclePaint); - canvas.restore(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java index 783780f8819c..c0053d194ff6 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java @@ -18,12 +18,13 @@ package com.android.systemui.bubbles; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.graphics.Point; +import android.graphics.Path; import android.graphics.Rect; import android.util.AttributeSet; import android.widget.ImageView; import com.android.internal.graphics.ColorUtils; +import com.android.launcher3.icons.DotRenderer; import com.android.systemui.R; /** @@ -31,16 +32,19 @@ import com.android.systemui.R; */ public class BadgedImageView extends ImageView { - private BadgeRenderer mDotRenderer; - private int mIconSize; private Rect mTempBounds = new Rect(); - private Point mTempPoint = new Point(); + private DotRenderer mDotRenderer; + private DotRenderer.DrawParams mDrawParams; + private int mIconBitmapSize; + private int mDotColor; private float mDotScale = 0f; - private int mUpdateDotColor; - private boolean mShowUpdateDot; + private boolean mShowDot; private boolean mOnLeft; + /** Same as value in Launcher3 IconShape */ + static final int DEFAULT_PATH_SIZE = 100; + public BadgedImageView(Context context) { this(context, null); } @@ -56,69 +60,100 @@ public class BadgedImageView extends ImageView { public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - mIconSize = getResources().getDimensionPixelSize(R.dimen.individual_bubble_size); - mDotRenderer = new BadgeRenderer(getContext()); + mIconBitmapSize = getResources().getDimensionPixelSize(R.dimen.bubble_icon_bitmap_size); + mDrawParams = new DotRenderer.DrawParams(); TypedArray ta = context.obtainStyledAttributes( - new int[] {android.R.attr.colorBackgroundFloating}); + new int[]{android.R.attr.colorBackgroundFloating}); ta.recycle(); } @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); - if (mShowUpdateDot) { - getDrawingRect(mTempBounds); - mTempPoint.set((getWidth() - mIconSize) / 2, getPaddingTop()); - mDotRenderer.draw(canvas, mUpdateDotColor, mTempBounds, mDotScale, mTempPoint, - mOnLeft); + if (!mShowDot) { + return; + } + getDrawingRect(mTempBounds); + + mDrawParams.color = mDotColor; + mDrawParams.iconBounds = mTempBounds; + mDrawParams.leftAlign = mOnLeft; + mDrawParams.scale = mDotScale; + + if (mDotRenderer == null) { + Path circlePath = new Path(); + float radius = DEFAULT_PATH_SIZE * 0.5f; + circlePath.addCircle(radius /* x */, radius /* y */, radius, Path.Direction.CW); + mDotRenderer = new DotRenderer(mIconBitmapSize, circlePath, DEFAULT_PATH_SIZE); } + mDotRenderer.draw(canvas, mDrawParams); } /** * Set whether the dot should appear on left or right side of the view. */ - public void setDotPosition(boolean onLeft) { + void setDotOnLeft(boolean onLeft) { mOnLeft = onLeft; invalidate(); } - public boolean getDotPosition() { + boolean getDotOnLeft() { return mOnLeft; } /** * Set whether the dot should show or not. */ - public void setShowDot(boolean showBadge) { - mShowUpdateDot = showBadge; + void setShowDot(boolean showDot) { + mShowDot = showDot; invalidate(); } /** * @return whether the dot is being displayed. */ - public boolean isShowingDot() { - return mShowUpdateDot; + boolean isShowingDot() { + return mShowDot; } /** * The colour to use for the dot. */ public void setDotColor(int color) { - mUpdateDotColor = ColorUtils.setAlphaComponent(color, 255 /* alpha */); + mDotColor = ColorUtils.setAlphaComponent(color, 255 /* alpha */); + invalidate(); + } + + /** + * @param iconPath The new icon path to use when calculating dot position. + */ + public void drawDot(Path iconPath) { + mDotRenderer = new DotRenderer(mIconBitmapSize, iconPath, DEFAULT_PATH_SIZE); invalidate(); } /** * How big the dot should be, fraction from 0 to 1. */ - public void setDotScale(float fraction) { + void setDotScale(float fraction) { mDotScale = fraction; invalidate(); } - public float getDotScale() { - return mDotScale; + /** + * Return dot position relative to bubble view container bounds. + */ + float[] getDotCenter() { + float[] dotPosition; + if (mOnLeft) { + dotPosition = mDotRenderer.getLeftDotPosition(); + } else { + dotPosition = mDotRenderer.getRightDotPosition(); + } + getDrawingRect(mTempBounds); + float dotCenterX = mTempBounds.width() * dotPosition[0]; + float dotCenterY = mTempBounds.height() * dotPosition[1]; + return new float[]{dotCenterX, dotCenterY}; } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java index 5c6c39722900..c3cee35d37fb 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java @@ -20,38 +20,67 @@ import static android.view.Display.INVALID_DISPLAY; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Notification; +import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Parcelable; import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; import android.view.LayoutInflater; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.List; import java.util.Objects; /** * Encapsulates the data and UI elements of a bubble. */ class Bubble { - - private static final boolean DEBUG = false; private static final String TAG = "Bubble"; + private NotificationEntry mEntry; private final String mKey; private final String mGroupId; private String mAppName; - private final BubbleExpandedView.OnBubbleBlockedListener mListener; + private Drawable mUserBadgedAppIcon; private boolean mInflated; - public NotificationEntry entry; - BubbleView iconView; - BubbleExpandedView expandedView; + private BubbleView mIconView; + private BubbleExpandedView mExpandedView; + private long mLastUpdated; private long mLastAccessed; - private PackageManager mPm; + private boolean mIsRemoved; + + /** + * Whether this notification should be shown in the shade when it is also displayed as a bubble. + * + * <p>When a notification is a bubble we don't show it in the shade once the bubble has been + * expanded</p> + */ + private boolean mShowInShadeWhenBubble = true; + + /** + * Whether the bubble should show a dot for the notification indicating updated content. + */ + private boolean mShowBubbleUpdateDot = true; + + /** Whether flyout text should be suppressed, regardless of any other flags or state. */ + private boolean mSuppressFlyout; public static String groupId(NotificationEntry entry) { UserHandle user = entry.notification.getUser(); @@ -61,31 +90,27 @@ class Bubble { /** Used in tests when no UI is required. */ @VisibleForTesting(visibility = PRIVATE) Bubble(Context context, NotificationEntry e) { - this (context, e, null); - } - - Bubble(Context context, NotificationEntry e, - BubbleExpandedView.OnBubbleBlockedListener listener) { - entry = e; + mEntry = e; mKey = e.key; mLastUpdated = e.notification.getPostTime(); mGroupId = groupId(e); - mListener = listener; - mPm = context.getPackageManager(); + PackageManager pm = context.getPackageManager(); ApplicationInfo info; try { - info = mPm.getApplicationInfo( - entry.notification.getPackageName(), + info = pm.getApplicationInfo( + mEntry.notification.getPackageName(), PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_DIRECT_BOOT_AWARE); if (info != null) { - mAppName = String.valueOf(mPm.getApplicationLabel(info)); + mAppName = String.valueOf(pm.getApplicationLabel(info)); } + Drawable appIcon = pm.getApplicationIcon(mEntry.notification.getPackageName()); + mUserBadgedAppIcon = pm.getUserBadgedIcon(appIcon, mEntry.notification.getUser()); } catch (PackageManager.NameNotFoundException unused) { - mAppName = entry.notification.getPackageName(); + mAppName = mEntry.notification.getPackageName(); } } @@ -93,12 +118,16 @@ class Bubble { return mKey; } + public NotificationEntry getEntry() { + return mEntry; + } + public String getGroupId() { return mGroupId; } public String getPackageName() { - return entry.notification.getPackageName(); + return mEntry.notification.getPackageName(); } public String getAppName() { @@ -109,9 +138,23 @@ class Bubble { return mInflated; } - public void updateDotVisibility() { - if (iconView != null) { - iconView.updateDotVisibility(true /* animate */); + void updateDotVisibility() { + if (mIconView != null) { + mIconView.updateDotVisibility(true /* animate */); + } + } + + BubbleView getIconView() { + return mIconView; + } + + BubbleExpandedView getExpandedView() { + return mExpandedView; + } + + void cleanupExpandedState() { + if (mExpandedView != null) { + mExpandedView.cleanUpExpandedState(); } } @@ -119,14 +162,14 @@ class Bubble { if (mInflated) { return; } - iconView = (BubbleView) inflater.inflate( + mIconView = (BubbleView) inflater.inflate( R.layout.bubble_view, stackView, false /* attachToRoot */); - iconView.setNotif(entry); + mIconView.setBubble(this); + mIconView.setAppIcon(mUserBadgedAppIcon); - expandedView = (BubbleExpandedView) inflater.inflate( + mExpandedView = (BubbleExpandedView) inflater.inflate( R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); - expandedView.setEntry(entry, stackView, mAppName); - expandedView.setOnBlockedListener(mListener); + mExpandedView.setBubble(this, stackView, mAppName); mInflated = true; } @@ -140,46 +183,38 @@ class Bubble { * and setting {@code false} actually means rendering the expanded view in transparent. */ void setContentVisibility(boolean visibility) { - if (expandedView != null) { - expandedView.setContentVisibility(visibility); + if (mExpandedView != null) { + mExpandedView.setContentVisibility(visibility); } } - void setDismissed() { - entry.setBubbleDismissed(true); - // TODO: move this somewhere where it can be guaranteed not to run until safe from flicker - if (expandedView != null) { - expandedView.cleanUpExpandedState(); - } - } - - void setEntry(NotificationEntry entry) { - this.entry = entry; + void updateEntry(NotificationEntry entry) { + mEntry = entry; mLastUpdated = entry.notification.getPostTime(); if (mInflated) { - iconView.update(entry); - expandedView.update(entry); + mIconView.update(this); + mExpandedView.update(this); } } /** * @return the newer of {@link #getLastUpdateTime()} and {@link #getLastAccessTime()} */ - public long getLastActivity() { + long getLastActivity() { return Math.max(mLastUpdated, mLastAccessed); } /** * @return the timestamp in milliseconds of the most recent notification entry for this bubble */ - public long getLastUpdateTime() { + long getLastUpdateTime() { return mLastUpdated; } /** * @return the timestamp in milliseconds when this bubble was last displayed in expanded state */ - public long getLastAccessTime() { + long getLastAccessTime() { return mLastAccessed; } @@ -187,7 +222,7 @@ class Bubble { * @return the display id of the virtual display on which bubble contents is drawn. */ int getDisplayId() { - return expandedView != null ? expandedView.getVirtualDisplayId() : INVALID_DISPLAY; + return mExpandedView != null ? mExpandedView.getVirtualDisplayId() : INVALID_DISPLAY; } /** @@ -195,14 +230,204 @@ class Bubble { */ void markAsAccessedAt(long lastAccessedMillis) { mLastAccessed = lastAccessedMillis; - entry.setShowInShadeWhenBubble(false); + setShowInShadeWhenBubble(false); + setShowBubbleDot(false); + } + + /** + * Whether this notification should be shown in the shade when it is also displayed as a + * bubble. + */ + boolean showInShadeWhenBubble() { + return !mEntry.isRowDismissed() && !shouldSuppressNotification() + && (!mEntry.isClearable() || mShowInShadeWhenBubble); + } + + /** + * Sets whether this notification should be shown in the shade when it is also displayed as a + * bubble. + */ + void setShowInShadeWhenBubble(boolean showInShade) { + mShowInShadeWhenBubble = showInShade; + } + + /** + * Sets whether the bubble for this notification should show a dot indicating updated content. + */ + void setShowBubbleDot(boolean showDot) { + mShowBubbleUpdateDot = showDot; } /** - * @return whether bubble is from a notification associated with a foreground service. + * Whether the bubble for this notification should show a dot indicating updated content. */ - public boolean isOngoing() { - return entry.isForegroundService(); + boolean showBubbleDot() { + return mShowBubbleUpdateDot && !mEntry.shouldSuppressNotificationDot(); + } + + /** + * Whether the flyout for the bubble should be shown. + */ + boolean showFlyoutForBubble() { + return !mSuppressFlyout && !mEntry.shouldSuppressPeek() + && !mEntry.shouldSuppressNotificationList(); + } + + /** + * Set whether the flyout text for the bubble should be shown when an update is received. + * + * @param suppressFlyout whether the flyout text is shown + */ + void setSuppressFlyout(boolean suppressFlyout) { + mSuppressFlyout = suppressFlyout; + } + + /** + * Returns whether the notification for this bubble is a foreground service. It shows that this + * is an ongoing bubble. + */ + boolean isOngoing() { + int flags = mEntry.notification.getNotification().flags; + return (flags & Notification.FLAG_FOREGROUND_SERVICE) != 0; + } + + float getDesiredHeight(Context context) { + Notification.BubbleMetadata data = mEntry.getBubbleMetadata(); + boolean useRes = data.getDesiredHeightResId() != 0; + if (useRes) { + return getDimenForPackageUser(context, data.getDesiredHeightResId(), + mEntry.notification.getPackageName(), + mEntry.notification.getUser().getIdentifier()); + } else { + return data.getDesiredHeight() + * context.getResources().getDisplayMetrics().density; + } + } + + String getDesiredHeightString() { + Notification.BubbleMetadata data = mEntry.getBubbleMetadata(); + boolean useRes = data.getDesiredHeightResId() != 0; + if (useRes) { + return String.valueOf(data.getDesiredHeightResId()); + } else { + return String.valueOf(data.getDesiredHeight()); + } + } + + @Nullable + PendingIntent getBubbleIntent(Context context) { + Notification notif = mEntry.notification.getNotification(); + Notification.BubbleMetadata data = notif.getBubbleMetadata(); + if (BubbleController.canLaunchInActivityView(context, mEntry) && data != null) { + return data.getIntent(); + } + return null; + } + + Intent getSettingsIntent() { + final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); + intent.putExtra(Settings.EXTRA_APP_UID, mEntry.notification.getUid()); + intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + return intent; + } + + /** + * Returns our best guess for the most relevant text summary of the latest update to this + * notification, based on its type. Returns null if there should not be an update message. + */ + CharSequence getUpdateMessage(Context context) { + final Notification underlyingNotif = mEntry.notification.getNotification(); + final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle(); + + try { + if (Notification.BigTextStyle.class.equals(style)) { + // Return the big text, it is big so probably important. If it's not there use the + // normal text. + CharSequence bigText = + underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT); + return !TextUtils.isEmpty(bigText) + ? bigText + : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); + } else if (Notification.MessagingStyle.class.equals(style)) { + final List<Notification.MessagingStyle.Message> messages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + (Parcelable[]) underlyingNotif.extras.get( + Notification.EXTRA_MESSAGES)); + + final Notification.MessagingStyle.Message latestMessage = + Notification.MessagingStyle.findLatestIncomingMessage(messages); + + if (latestMessage != null) { + final CharSequence personName = latestMessage.getSenderPerson() != null + ? latestMessage.getSenderPerson().getName() + : null; + + // Prepend the sender name if available since group chats also use messaging + // style. + if (!TextUtils.isEmpty(personName)) { + return context.getResources().getString( + R.string.notification_summary_message_format, + personName, + latestMessage.getText()); + } else { + return latestMessage.getText(); + } + } + } else if (Notification.InboxStyle.class.equals(style)) { + CharSequence[] lines = + underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES); + + // Return the last line since it should be the most recent. + if (lines != null && lines.length > 0) { + return lines[lines.length - 1]; + } + } else if (Notification.MediaStyle.class.equals(style)) { + // Return nothing, media updates aren't typically useful as a text update. + return null; + } else { + // Default to text extra. + return underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); + } + } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) { + // No use crashing, we'll just return null and the caller will assume there's no update + // message. + e.printStackTrace(); + } + + return null; + } + + private int getDimenForPackageUser(Context context, int resId, String pkg, int userId) { + PackageManager pm = context.getPackageManager(); + Resources r; + if (pkg != null) { + try { + if (userId == UserHandle.USER_ALL) { + userId = UserHandle.USER_SYSTEM; + } + r = pm.getResourcesForApplicationAsUser(pkg, userId); + return r.getDimensionPixelSize(resId); + } catch (PackageManager.NameNotFoundException ex) { + // Uninstalled, don't care + } catch (Resources.NotFoundException e) { + // Invalid res id, return 0 and user our default + Log.e(TAG, "Couldn't find desired height res id", e); + } + } + return 0; + } + + private boolean shouldSuppressNotification() { + return mEntry.getBubbleMetadata() != null + && mEntry.getBubbleMetadata().isNotificationSuppressed(); + } + + boolean shouldAutoExpand() { + Notification.BubbleMetadata metadata = mEntry.getBubbleMetadata(); + return metadata != null && metadata.getAutoExpandBubble(); } @Override @@ -210,6 +435,20 @@ class Bubble { return "Bubble{" + mKey + '}'; } + /** + * Description of current bubble state. + */ + public void dump( + @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { + pw.print("key: "); pw.println(mKey); + pw.print(" showInShade: "); pw.println(showInShadeWhenBubble()); + pw.print(" showDot: "); pw.println(showBubbleDot()); + pw.print(" showFlyout: "); pw.println(showFlyoutForBubble()); + pw.print(" desiredHeight: "); pw.println(getDesiredHeightString()); + pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification()); + pw.print(" autoExpand: "); pw.println(shouldAutoExpand()); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index a23c99ef01fe..94d9ede5c8b1 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -16,21 +16,24 @@ package com.android.systemui.bubbles; +import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; import static android.app.Notification.FLAG_BUBBLE; -import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE; -import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST; -import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL; import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL; +import static android.service.notification.NotificationListenerService.REASON_CLICK; +import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER; +import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON; @@ -39,9 +42,8 @@ import static java.lang.annotation.ElementType.LOCAL_VARIABLE; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.RetentionPolicy.SOURCE; -import android.app.ActivityManager; +import android.annotation.UserIdInt; import android.app.ActivityManager.RunningTaskInfo; -import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; @@ -53,10 +55,11 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.provider.Settings; import android.service.notification.NotificationListenerService.RankingMap; -import android.service.notification.StatusBarNotification; import android.service.notification.ZenModeConfig; +import android.util.ArraySet; import android.util.Log; import android.util.Pair; +import android.util.SparseSetArray; import android.view.Display; import android.view.IPinnedStackController; import android.view.IPinnedStackListener; @@ -75,18 +78,23 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.WindowManagerWrapper; +import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationRemoveInterceptor; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; +import com.android.systemui.statusbar.notification.collection.NotificationData; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag; +import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.StatusBarWindowController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ZenModeController; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import java.util.ArrayList; import java.util.List; import javax.inject.Inject; @@ -101,12 +109,12 @@ import javax.inject.Singleton; @Singleton public class BubbleController implements ConfigurationController.ConfigurationListener { - private static final String TAG = "BubbleController"; - private static final boolean DEBUG = false; + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; @Retention(SOURCE) @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED, - DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE}) + DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, + DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT}) @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @interface DismissReason {} @@ -117,24 +125,14 @@ public class BubbleController implements ConfigurationController.ConfigurationLi static final int DISMISS_NOTIF_CANCEL = 5; static final int DISMISS_ACCESSIBILITY_ACTION = 6; static final int DISMISS_NO_LONGER_BUBBLE = 7; + static final int DISMISS_USER_CHANGED = 8; + static final int DISMISS_GROUP_CANCELLED = 9; + static final int DISMISS_INVALID_INTENT = 10; public static final int MAX_BUBBLES = 5; // TODO: actually enforce this - // Enables some subset of notifs to automatically become bubbles - public static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false; - /** Flag to enable or disable the entire feature */ private static final String ENABLE_BUBBLES = "experiment_enable_bubbles"; - /** Auto bubble flags set whether different notif types should be presented as a bubble */ - private static final String ENABLE_AUTO_BUBBLE_MESSAGES = "experiment_autobubble_messaging"; - private static final String ENABLE_AUTO_BUBBLE_ONGOING = "experiment_autobubble_ongoing"; - private static final String ENABLE_AUTO_BUBBLE_ALL = "experiment_autobubble_all"; - - /** Use an activityView for an auto-bubbled notifs if it has an appropriate content intent */ - private static final String ENABLE_BUBBLE_CONTENT_INTENT = "experiment_bubble_content_intent"; - - private static final String BUBBLE_STIFFNESS = "experiment_bubble_stiffness"; - private static final String BUBBLE_BOUNCINESS = "experiment_bubble_bounciness"; private final Context mContext; private final NotificationEntryManager mNotificationEntryManager; @@ -142,10 +140,16 @@ public class BubbleController implements ConfigurationController.ConfigurationLi private BubbleStateChangeListener mStateChangeListener; private BubbleExpandListener mExpandListener; @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer; + private final NotificationGroupManager mNotificationGroupManager; private BubbleData mBubbleData; @Nullable private BubbleStackView mStackView; + // Tracks the id of the current (foreground) user. + private int mCurrentUserId; + // Saves notification keys of active bubbles when users are switched. + private final SparseSetArray<String> mSavedBubbleKeysPerUser; + // Bubbles get added to the status bar view private final StatusBarWindowController mStatusBarWindowController; private final ZenModeController mZenModeController; @@ -157,6 +161,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi // Used for determining view rect for touch interaction private Rect mTempRect = new Rect(); + // Listens to user switch so bubbles can be saved and restored. + private final NotificationLockscreenUserManager mNotifUserManager; + /** Last known orientation, used to detect orientation changes in {@link #onConfigChanged}. */ private int mOrientation = Configuration.ORIENTATION_UNDEFINED; @@ -211,28 +218,38 @@ public class BubbleController implements ConfigurationController.ConfigurationLi public BubbleController(Context context, StatusBarWindowController statusBarWindowController, BubbleData data, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, - ZenModeController zenModeController) { + ZenModeController zenModeController, + NotificationLockscreenUserManager notifUserManager, + NotificationGroupManager groupManager) { this(context, statusBarWindowController, data, null /* synchronizer */, - configurationController, interruptionStateProvider, zenModeController); + configurationController, interruptionStateProvider, zenModeController, + notifUserManager, groupManager); } public BubbleController(Context context, StatusBarWindowController statusBarWindowController, BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, - ZenModeController zenModeController) { + ZenModeController zenModeController, + NotificationLockscreenUserManager notifUserManager, + NotificationGroupManager groupManager) { mContext = context; mNotificationInterruptionStateProvider = interruptionStateProvider; + mNotifUserManager = notifUserManager; mZenModeController = zenModeController; mZenModeController.addCallback(new ZenModeController.Callback() { @Override public void onZenChanged(int zen) { - updateStackViewForZenConfig(); + if (mStackView != null) { + mStackView.updateDots(); + } } @Override public void onConfigChanged(ZenModeConfig config) { - updateStackViewForZenConfig(); + if (mStackView != null) { + mStackView.updateDots(); + } } }); @@ -244,6 +261,24 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mNotificationEntryManager = Dependency.get(NotificationEntryManager.class); mNotificationEntryManager.addNotificationEntryListener(mEntryListener); mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor); + mNotificationGroupManager = groupManager; + mNotificationGroupManager.addOnGroupChangeListener( + new NotificationGroupManager.OnGroupChangeListener() { + @Override + public void onGroupSuppressionChanged( + NotificationGroupManager.NotificationGroup group, + boolean suppressed) { + // More notifications could be added causing summary to no longer + // be suppressed -- in this case need to remove the key. + final String groupKey = group.summary != null + ? group.summary.notification.getGroupKey() + : null; + if (!suppressed && groupKey != null + && mBubbleData.isSummarySuppressed(groupKey)) { + mBubbleData.removeSuppressedSummary(groupKey); + } + } + }); mStatusBarWindowController = statusBarWindowController; mStatusBarStateListener = new StatusBarStateListener(); @@ -261,6 +296,16 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mBarService = IStatusBarService.Stub.asInterface( ServiceManager.getService(Context.STATUS_BAR_SERVICE)); + + mSavedBubbleKeysPerUser = new SparseSetArray<>(); + mCurrentUserId = mNotifUserManager.getCurrentUserId(); + mNotifUserManager.addUserChangedListener( + newUserId -> { + saveBubbles(mCurrentUserId); + mBubbleData.dismissAll(DISMISS_USER_CHANGED); + restoreBubbles(newUserId); + mCurrentUserId = newUserId; + }); } /** @@ -271,19 +316,55 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (mStackView == null) { mStackView = new BubbleStackView(mContext, mBubbleData, mSurfaceSynchronizer); ViewGroup sbv = mStatusBarWindowController.getStatusBarView(); - // TODO(b/130237686): When you expand the shade on top of expanded bubble, there is no - // scrim between bubble and the shade - int bubblePosition = sbv.indexOfChild(sbv.findViewById(R.id.scrim_behind)) + 1; - sbv.addView(mStackView, bubblePosition, + int bubbleScrimIndex = sbv.indexOfChild(sbv.findViewById(R.id.scrim_for_bubble)); + int stackIndex = bubbleScrimIndex + 1; // Show stack above bubble scrim. + sbv.addView(mStackView, stackIndex, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); if (mExpandListener != null) { mStackView.setExpandListener(mExpandListener); } + } + } - updateStackViewForZenConfig(); + /** + * Records the notification key for any active bubbles. These are used to restore active + * bubbles when the user returns to the foreground. + * + * @param userId the id of the user + */ + private void saveBubbles(@UserIdInt int userId) { + // First clear any existing keys that might be stored. + mSavedBubbleKeysPerUser.remove(userId); + // Add in all active bubbles for the current user. + for (Bubble bubble: mBubbleData.getBubbles()) { + mSavedBubbleKeysPerUser.add(userId, bubble.getKey()); } } + /** + * Promotes existing notifications to Bubbles if they were previously bubbles. + * + * @param userId the id of the user + */ + private void restoreBubbles(@UserIdInt int userId) { + NotificationData notificationData = + mNotificationEntryManager.getNotificationData(); + ArraySet<String> savedBubbleKeys = mSavedBubbleKeysPerUser.get(userId); + if (savedBubbleKeys == null) { + // There were no bubbles saved for this used. + return; + } + for (NotificationEntry e : notificationData.getNotificationsForCurrentUser()) { + if (savedBubbleKeys.contains(e.key) + && mNotificationInterruptionStateProvider.shouldBubbleUp(e) + && canLaunchInActivityView(mContext, e)) { + updateBubble(e, /* suppressFlyout= */ true); + } + } + // Finally, remove the entries for this user now that bubbles are restored. + mSavedBubbleKeysPerUser.remove(mCurrentUserId); + } + @Override public void onUiModeChanged() { if (mStackView != null) { @@ -301,8 +382,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Override public void onConfigChanged(Configuration newConfig) { if (mStackView != null && newConfig != null && newConfig.orientation != mOrientation) { - mStackView.onOrientationChanged(); mOrientation = newConfig.orientation; + mStackView.onOrientationChanged(newConfig.orientation); } } @@ -360,6 +441,25 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mBubbleData.setExpanded(false /* expanded */); } + /** + * True if either: + * (1) There is a bubble associated with the provided key and if its notification is hidden + * from the shade. + * (2) There is a group summary associated with the provided key that is hidden from the shade + * because it has been dismissed but still has child bubbles active. + * + * False otherwise. + */ + public boolean isBubbleNotificationSuppressedFromShade(String key) { + boolean isBubbleAndSuppressed = mBubbleData.hasBubbleWithKey(key) + && !mBubbleData.getBubbleWithKey(key).showInShadeWhenBubble(); + NotificationEntry entry = mNotificationEntryManager.getNotificationData().get(key); + String groupKey = entry != null ? entry.notification.getGroupKey() : null; + boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey); + boolean isSummary = key.equals(mBubbleData.getSummaryKey(groupKey)); + return (isSummary && isSuppressedSummary) || isBubbleAndSuppressed; + } + void selectBubble(Bubble bubble) { mBubbleData.setSelectedBubble(bubble); } @@ -406,11 +506,15 @@ public class BubbleController implements ConfigurationController.ConfigurationLi * @param notif the notification associated with this bubble. */ void updateBubble(NotificationEntry notif) { + updateBubble(notif, /* supressFlyout */ false); + } + + void updateBubble(NotificationEntry notif, boolean suppressFlyout) { // If this is an interruptive notif, mark that it's interrupted if (notif.importance >= NotificationManager.IMPORTANCE_HIGH) { notif.setInterruption(); } - mBubbleData.notificationEntryUpdated(notif); + mBubbleData.notificationEntryUpdated(notif, suppressFlyout); } /** @@ -423,7 +527,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi // TEMP: refactor to change this to pass entry Bubble bubble = mBubbleData.getBubbleWithKey(key); if (bubble != null) { - mBubbleData.notificationEntryRemoved(bubble.entry, reason); + mBubbleData.notificationEntryRemoved(bubble.getEntry(), reason); } } @@ -432,33 +536,50 @@ public class BubbleController implements ConfigurationController.ConfigurationLi new NotificationRemoveInterceptor() { @Override public boolean onNotificationRemoveRequested(String key, int reason) { - if (!mBubbleData.hasBubbleWithKey(key)) { + NotificationEntry entry = mNotificationEntryManager.getNotificationData().get(key); + String groupKey = entry != null ? entry.notification.getGroupKey() : null; + ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey); + + boolean inBubbleData = mBubbleData.hasBubbleWithKey(key); + boolean isSuppressedSummary = (mBubbleData.isSummarySuppressed(groupKey) + && mBubbleData.getSummaryKey(groupKey).equals(key)); + boolean isSummary = entry != null + && entry.notification.getNotification().isGroupSummary(); + boolean isSummaryOfBubbles = (isSuppressedSummary || isSummary) + && bubbleChildren != null && !bubbleChildren.isEmpty(); + + if (!inBubbleData && !isSummaryOfBubbles) { return false; } - NotificationEntry entry = mBubbleData.getBubbleWithKey(key).entry; final boolean isClearAll = reason == REASON_CANCEL_ALL; - final boolean isUserDimiss = reason == REASON_CANCEL; + final boolean isUserDimiss = reason == REASON_CANCEL || reason == REASON_CLICK; final boolean isAppCancel = reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL; + final boolean isSummaryCancel = reason == REASON_GROUP_SUMMARY_CANCELED; // Need to check for !appCancel here because the notification may have // previously been dismissed & entry.isRowDismissed would still be true boolean userRemovedNotif = (entry.isRowDismissed() && !isAppCancel) - || isClearAll || isUserDimiss; + || isClearAll || isUserDimiss || isSummaryCancel; + + if (isSummaryOfBubbles) { + return handleSummaryRemovalInterception(entry, userRemovedNotif); + } // The bubble notification sticks around in the data as long as the bubble is // not dismissed and the app hasn't cancelled the notification. - boolean bubbleExtended = entry.isBubble() && !entry.isBubbleDismissed() - && userRemovedNotif; + Bubble bubble = mBubbleData.getBubbleWithKey(key); + boolean bubbleExtended = entry.isBubble() && userRemovedNotif; if (bubbleExtended) { - entry.setShowInShadeWhenBubble(false); + bubble.setShowInShadeWhenBubble(false); + bubble.setShowBubbleDot(false); if (mStackView != null) { mStackView.updateDotVisibility(entry.key); } mNotificationEntryManager.updateNotifications(); return true; - } else if (!userRemovedNotif && !entry.isBubbleDismissed()) { + } else if (!userRemovedNotif) { // This wasn't a user removal so we should remove the bubble as well mBubbleData.notificationEntryRemoved(entry, DISMISS_NOTIF_CANCEL); return false; @@ -467,21 +588,56 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } }; - @SuppressWarnings("FieldCanBeLocal") - private final NotificationEntryListener mEntryListener = new NotificationEntryListener() { - @Override - public void onPendingEntryAdded(NotificationEntry entry) { - if (!areBubblesEnabled(mContext)) { - return; + private boolean handleSummaryRemovalInterception(NotificationEntry summary, + boolean userRemovedNotif) { + String groupKey = summary.notification.getGroupKey(); + ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey); + + if (userRemovedNotif) { + // If it's a user dismiss we mark the children to be hidden from the shade. + for (int i = 0; i < bubbleChildren.size(); i++) { + Bubble bubbleChild = bubbleChildren.get(i); + // As far as group manager is concerned, once a child is no longer shown + // in the shade, it is essentially removed. + mNotificationGroupManager.onEntryRemoved(bubbleChild.getEntry()); + bubbleChild.setShowInShadeWhenBubble(false); + bubbleChild.setShowBubbleDot(false); + if (mStackView != null) { + mStackView.updateDotVisibility(bubbleChild.getKey()); + } } - if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry) - && canLaunchInActivityView(mContext, entry)) { - updateShowInShadeForSuppressNotification(entry); + // And since all children are removed, remove the summary. + mNotificationGroupManager.onEntryRemoved(summary); + + // If the summary was auto-generated we don't need to keep that notification around + // because apps can't cancel it; so we only intercept & suppress real summaries. + boolean isAutogroupSummary = (summary.notification.getNotification().flags + & FLAG_AUTOGROUP_SUMMARY) != 0; + if (!isAutogroupSummary) { + mBubbleData.addSummaryToSuppress(summary.notification.getGroupKey(), + summary.key); + // Tell shade to update for the suppression + mNotificationEntryManager.updateNotifications(); } + return !isAutogroupSummary; + } else { + // If it's not a user dismiss it's a cancel. + mBubbleData.removeSuppressedSummary(groupKey); + + // Remove any associated bubble children. + for (int i = 0; i < bubbleChildren.size(); i++) { + Bubble bubbleChild = bubbleChildren.get(i); + mBubbleData.notificationEntryRemoved(bubbleChild.getEntry(), + DISMISS_GROUP_CANCELLED); + } + return false; } + } + @SuppressWarnings("FieldCanBeLocal") + private final NotificationEntryListener mEntryListener = new NotificationEntryListener() { @Override - public void onEntryInflated(NotificationEntry entry, @InflationFlag int inflatedFlags) { + public void onPendingEntryAdded(NotificationEntry entry) { if (!areBubblesEnabled(mContext)) { return; } @@ -502,8 +658,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi // It was previously a bubble but no longer a bubble -- lets remove it removeBubble(entry.key, DISMISS_NO_LONGER_BUBBLE); } else if (shouldBubble) { - updateShowInShadeForSuppressNotification(entry); - entry.setBubbleDismissed(false); // updates come back as bubbles even if dismissed + Bubble b = mBubbleData.getBubbleWithKey(entry.key); updateBubble(entry); } } @@ -540,27 +695,60 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } // Do removals, if any. - for (Pair<Bubble, Integer> removed : update.removedBubbles) { + ArrayList<Pair<Bubble, Integer>> removedBubbles = + new ArrayList<>(update.removedBubbles); + for (Pair<Bubble, Integer> removed : removedBubbles) { final Bubble bubble = removed.first; @DismissReason final int reason = removed.second; mStackView.removeBubble(bubble); - if (!mBubbleData.hasBubbleWithKey(bubble.getKey()) - && !bubble.entry.showInShadeWhenBubble()) { - // The bubble is gone & the notification is gone, time to actually remove it - mNotificationEntryManager.performRemoveNotification(bubble.entry.notification, - UNDEFINED_DISMISS_REASON); - } else { - // Update the flag for SysUI - bubble.entry.notification.getNotification().flags &= ~FLAG_BUBBLE; - - // Make sure NoMan knows it's not a bubble anymore so anyone querying it will - // get right result back - try { - mBarService.onNotificationBubbleChanged(bubble.getKey(), - false /* isBubble */); - } catch (RemoteException e) { - // Bad things have happened + // If the bubble is removed for user switching, leave the notification in place. + if (reason != DISMISS_USER_CHANGED) { + if (!mBubbleData.hasBubbleWithKey(bubble.getKey()) + && !bubble.showInShadeWhenBubble()) { + // The bubble is gone & the notification is gone, time to actually remove it + mNotificationEntryManager.performRemoveNotification( + bubble.getEntry().notification, UNDEFINED_DISMISS_REASON); + } else { + // Update the flag for SysUI + bubble.getEntry().notification.getNotification().flags &= ~FLAG_BUBBLE; + + // Make sure NoMan knows it's not a bubble anymore so anyone querying it + // will get right result back + try { + mBarService.onNotificationBubbleChanged(bubble.getKey(), + false /* isBubble */); + } catch (RemoteException e) { + // Bad things have happened + } + } + + // Check if removed bubble has an associated suppressed group summary that needs + // to be removed now. + final String groupKey = bubble.getEntry().notification.getGroupKey(); + if (mBubbleData.isSummarySuppressed(groupKey) + && mBubbleData.getBubblesInGroup(groupKey).isEmpty()) { + // Time to actually remove the summary. + String notifKey = mBubbleData.getSummaryKey(groupKey); + mBubbleData.removeSuppressedSummary(groupKey); + NotificationEntry entry = + mNotificationEntryManager.getNotificationData().get(notifKey); + mNotificationEntryManager.performRemoveNotification( + entry.notification, UNDEFINED_DISMISS_REASON); + } + + // Check if summary should be removed from NoManGroup + NotificationEntry summary = mNotificationGroupManager.getLogicalGroupSummary( + bubble.getEntry().notification); + if (summary != null) { + ArrayList<NotificationEntry> summaryChildren = + mNotificationGroupManager.getLogicalChildren(summary.notification); + boolean isSummaryThisNotif = summary.key.equals(bubble.getEntry().key); + if (!isSummaryThisNotif + && (summaryChildren == null || summaryChildren.isEmpty())) { + mNotificationEntryManager.performRemoveNotification( + summary.notification, UNDEFINED_DISMISS_REASON); + } } } } @@ -575,6 +763,10 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (update.selectionChanged) { mStackView.setSelectedBubble(update.selectedBubble); + if (update.selectedBubble != null) { + mNotificationGroupManager.updateSuppression( + update.selectedBubble.getEntry()); + } } // Expanding? Apply this last. @@ -585,7 +777,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mNotificationEntryManager.updateNotifications(); updateStack(); - if (DEBUG) { + if (DEBUG_BUBBLE_CONTROLLER) { Log.d(TAG, "[BubbleData]"); Log.d(TAG, formatBubblesString(mBubbleData.getBubbles(), mBubbleData.getSelectedBubble())); @@ -600,34 +792,6 @@ public class BubbleController implements ConfigurationController.ConfigurationLi }; /** - * Updates the stack view's suppression flags from the latest config from the zen (do not - * disturb) controller. - */ - private void updateStackViewForZenConfig() { - final ZenModeConfig zenModeConfig = mZenModeController.getConfig(); - - if (zenModeConfig == null || mStackView == null) { - return; - } - - final int suppressedEffects = zenModeConfig.suppressedVisualEffects; - final boolean hideNotificationDotsSelected = - (suppressedEffects & SUPPRESSED_EFFECT_BADGE) != 0; - final boolean dontPopNotifsOnScreenSelected = - (suppressedEffects & SUPPRESSED_EFFECT_PEEK) != 0; - final boolean hideFromPullDownShadeSelected = - (suppressedEffects & SUPPRESSED_EFFECT_NOTIFICATION_LIST) != 0; - - final boolean dndEnabled = mZenModeController.getZen() != Settings.Global.ZEN_MODE_OFF; - - mStackView.setSuppressNewDot( - dndEnabled && hideNotificationDotsSelected); - mStackView.setSuppressFlyout( - dndEnabled && (dontPopNotifsOnScreenSelected - || hideFromPullDownShadeSelected)); - } - - /** * Lets any listeners know if bubble state has changed. * Updates the visibility of the bubbles based on current state. * Does not un-bubble, just hides or un-hides. Notifies any @@ -697,46 +861,16 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } /** - * Whether the notification should automatically bubble or not. Gated by secure settings flags. + * Description of current bubble state. */ - @VisibleForTesting - protected boolean shouldAutoBubbleForFlags(Context context, NotificationEntry entry) { - if (entry.isBubbleDismissed()) { - return false; - } - StatusBarNotification n = entry.notification; - - boolean autoBubbleMessages = shouldAutoBubbleMessages(context) || DEBUG_ENABLE_AUTO_BUBBLE; - boolean autoBubbleOngoing = shouldAutoBubbleOngoing(context) || DEBUG_ENABLE_AUTO_BUBBLE; - boolean autoBubbleAll = shouldAutoBubbleAll(context) || DEBUG_ENABLE_AUTO_BUBBLE; - - boolean hasRemoteInput = false; - if (n.getNotification().actions != null) { - for (Notification.Action action : n.getNotification().actions) { - if (action.getRemoteInputs() != null) { - hasRemoteInput = true; - break; - } - } + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("BubbleController state:"); + mBubbleData.dump(fd, pw, args); + pw.println(); + if (mStackView != null) { + mStackView.dump(fd, pw, args); } - boolean isCall = Notification.CATEGORY_CALL.equals(n.getNotification().category) - && n.isOngoing(); - boolean isMusic = n.getNotification().hasMediaSession(); - boolean isImportantOngoing = isMusic || isCall; - - Class<? extends Notification.Style> style = n.getNotification().getNotificationStyle(); - boolean isMessageType = Notification.CATEGORY_MESSAGE.equals(n.getNotification().category); - boolean isMessageStyle = Notification.MessagingStyle.class.equals(style); - return (((isMessageType && hasRemoteInput) || isMessageStyle) && autoBubbleMessages) - || (isImportantOngoing && autoBubbleOngoing) - || autoBubbleAll; - } - - private void updateShowInShadeForSuppressNotification(NotificationEntry entry) { - boolean suppressNotification = entry.getBubbleMetadata() != null - && entry.getBubbleMetadata().isNotificationSuppressed() - && isForegroundApp(mContext, entry.notification.getPackageName()); - entry.setShowInShadeWhenBubble(!suppressNotification); + pw.println(); } static String formatBubblesString(List<Bubble> bubbles, Bubble selected) { @@ -757,18 +891,6 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } /** - * Return true if the applications with the package name is running in foreground. - * - * @param context application context. - * @param pkgName application package name. - */ - public static boolean isForegroundApp(Context context, String pkgName) { - ActivityManager am = context.getSystemService(ActivityManager.class); - List<RunningTaskInfo> tasks = am.getRunningTasks(1 /* maxNum */); - return !tasks.isEmpty() && pkgName.equals(tasks.get(0).topActivity.getPackageName()); - } - - /** * This task stack listener is responsible for responding to tasks moved to the front * which are on the default (main) display. When this happens, expanded bubbles must be * collapsed so the user may interact with the app which was just moved to the front. @@ -782,7 +904,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Override public void onTaskMovedToFront(RunningTaskInfo taskInfo) { if (mStackView != null && taskInfo.displayId == Display.DEFAULT_DISPLAY) { - mBubbleData.setExpanded(false); + if (!mStackView.isExpansionAnimating()) { + mBubbleData.setExpanded(false); + } } } @@ -807,26 +931,18 @@ public class BubbleController implements ConfigurationController.ConfigurationLi expandedBubble.setContentVisibility(true); } } - } - - private static boolean shouldAutoBubbleMessages(Context context) { - return Settings.Secure.getInt(context.getContentResolver(), - ENABLE_AUTO_BUBBLE_MESSAGES, 0) != 0; - } - - private static boolean shouldAutoBubbleOngoing(Context context) { - return Settings.Secure.getInt(context.getContentResolver(), - ENABLE_AUTO_BUBBLE_ONGOING, 0) != 0; - } - - private static boolean shouldAutoBubbleAll(Context context) { - return Settings.Secure.getInt(context.getContentResolver(), - ENABLE_AUTO_BUBBLE_ALL, 0) != 0; - } - static boolean shouldUseContentIntent(Context context) { - return Settings.Secure.getInt(context.getContentResolver(), - ENABLE_BUBBLE_CONTENT_INTENT, 0) != 0; + @Override + public void onSingleTaskDisplayEmpty(int displayId) { + final Bubble expandedBubble = getExpandedBubble(mContext); + if (expandedBubble == null) { + return; + } + if (expandedBubble.getDisplayId() == displayId) { + mBubbleData.setExpanded(false); + expandedBubble.getExpandedView().notifyDisplayEmpty(); + } + } } private static boolean areBubblesEnabled(Context context) { @@ -834,20 +950,6 @@ public class BubbleController implements ConfigurationController.ConfigurationLi ENABLE_BUBBLES, 1) != 0; } - /** Default stiffness to use for bubble physics animations. */ - public static int getBubbleStiffness(Context context, int defaultStiffness) { - return Settings.Secure.getInt( - context.getContentResolver(), BUBBLE_STIFFNESS, defaultStiffness); - } - - /** Default bounciness/damping ratio to use for bubble physics animations. */ - public static float getBubbleBounciness(Context context, float defaultBounciness) { - return Settings.Secure.getInt( - context.getContentResolver(), - BUBBLE_BOUNCINESS, - (int) (defaultBounciness * 100)) / 100f; - } - /** * Whether an intent is properly configured to display in an {@link android.app.ActivityView}. * diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java index 5575b35a12ae..81c8da8247ec 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java @@ -16,6 +16,9 @@ package com.android.systemui.bubbles; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; +import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA; +import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; import static java.util.stream.Collectors.toList; @@ -33,11 +36,12 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.bubbles.BubbleController.DismissReason; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -51,8 +55,7 @@ import javax.inject.Singleton; @Singleton public class BubbleData { - private static final String TAG = "BubbleData"; - private static final boolean DEBUG = false; + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleData" : TAG_BUBBLES; private static final int MAX_BUBBLES = 5; @@ -123,6 +126,19 @@ public class BubbleData { @Nullable private Listener mListener; + /** + * We track groups with summaries that aren't visibly displayed but still kept around because + * the bubble(s) associated with the summary still exist. + * + * The summary must be kept around so that developers can cancel it (and hence the bubbles + * associated with it). This list is used to check if the summary should be hidden from the + * shade. + * + * Key: group key of the NotificationEntry + * Value: key of the NotificationEntry + */ + private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>(); + @Inject public BubbleData(Context context) { mContext = context; @@ -148,7 +164,7 @@ public class BubbleData { } public void setExpanded(boolean expanded) { - if (DEBUG) { + if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "setExpanded: " + expanded); } setExpandedInternal(expanded); @@ -156,29 +172,30 @@ public class BubbleData { } public void setSelectedBubble(Bubble bubble) { - if (DEBUG) { + if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "setSelectedBubble: " + bubble); } setSelectedBubbleInternal(bubble); dispatchPendingChanges(); } - public void notificationEntryUpdated(NotificationEntry entry) { - if (DEBUG) { + void notificationEntryUpdated(NotificationEntry entry, boolean suppressFlyout) { + if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "notificationEntryUpdated: " + entry); } Bubble bubble = getBubbleWithKey(entry.key); if (bubble == null) { // Create a new bubble - bubble = new Bubble(mContext, entry, this::onBubbleBlocked); + bubble = new Bubble(mContext, entry); + bubble.setSuppressFlyout(suppressFlyout); doAdd(bubble); trim(); } else { // Updates an existing bubble - bubble.setEntry(entry); + bubble.updateEntry(entry); doUpdate(bubble); } - if (shouldAutoExpand(entry)) { + if (bubble.shouldAutoExpand()) { setSelectedBubbleInternal(bubble); if (!mExpanded) { setExpandedInternal(true); @@ -186,11 +203,14 @@ public class BubbleData { } else if (mSelectedBubble == null) { setSelectedBubbleInternal(bubble); } + boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble; + bubble.setShowInShadeWhenBubble(!isBubbleExpandedAndSelected); + bubble.setShowBubbleDot(!isBubbleExpandedAndSelected); dispatchPendingChanges(); } public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) { - if (DEBUG) { + if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "notificationEntryRemoved: entry=" + entry + " reason=" + reason); } doRemove(entry.key, reason); @@ -222,8 +242,59 @@ public class BubbleData { dispatchPendingChanges(); } + /** + * Adds a group key indicating that the summary for this group should be suppressed. + * + * @param groupKey the group key of the group whose summary should be suppressed. + * @param notifKey the notification entry key of that summary. + */ + void addSummaryToSuppress(String groupKey, String notifKey) { + mSuppressedGroupKeys.put(groupKey, notifKey); + } + + /** + * Retrieves the notif entry key of the summary associated with the provided group key. + * + * @param groupKey the group to look up + * @return the key for the {@link NotificationEntry} that is the summary of this group. + */ + String getSummaryKey(String groupKey) { + return mSuppressedGroupKeys.get(groupKey); + } + + /** + * Removes a group key indicating that summary for this group should no longer be suppressed. + */ + void removeSuppressedSummary(String groupKey) { + mSuppressedGroupKeys.remove(groupKey); + } + + /** + * Whether the summary for the provided group key is suppressed. + */ + boolean isSummarySuppressed(String groupKey) { + return mSuppressedGroupKeys.containsKey(groupKey); + } + + /** + * Retrieves any bubbles that are part of the notification group represented by the provided + * group key. + */ + ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) { + ArrayList<Bubble> bubbleChildren = new ArrayList<>(); + if (groupKey == null) { + return bubbleChildren; + } + for (Bubble b : mBubbles) { + if (groupKey.equals(b.getEntry().notification.getGroupKey())) { + bubbleChildren.add(b); + } + } + return bubbleChildren; + } + private void doAdd(Bubble bubble) { - if (DEBUG) { + if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "doAdd: " + bubble); } int minInsertPoint = 0; @@ -256,7 +327,7 @@ public class BubbleData { } private void doUpdate(Bubble bubble) { - if (DEBUG) { + if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "doUpdate: " + bubble); } mStateChange.updatedBubble = bubble; @@ -301,12 +372,11 @@ public class BubbleData { Bubble newSelected = mBubbles.get(newIndex); setSelectedBubbleInternal(newSelected); } - bubbleToRemove.setDismissed(); - maybeSendDeleteIntent(reason, bubbleToRemove.entry); + maybeSendDeleteIntent(reason, bubbleToRemove.getEntry()); } public void dismissAll(@DismissReason int reason) { - if (DEBUG) { + if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "dismissAll: reason=" + reason); } if (mBubbles.isEmpty()) { @@ -316,8 +386,7 @@ public class BubbleData { setSelectedBubbleInternal(null); while (!mBubbles.isEmpty()) { Bubble bubble = mBubbles.remove(0); - bubble.setDismissed(); - maybeSendDeleteIntent(reason, bubble.entry); + maybeSendDeleteIntent(reason, bubble.getEntry()); mStateChange.bubbleRemoved(bubble, reason); } dispatchPendingChanges(); @@ -336,7 +405,7 @@ public class BubbleData { * @param bubble the new selected bubble */ private void setSelectedBubbleInternal(@Nullable Bubble bubble) { - if (DEBUG) { + if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "setSelectedBubbleInternal: " + bubble); } if (Objects.equals(bubble, mSelectedBubble)) { @@ -361,7 +430,7 @@ public class BubbleData { * @param shouldExpand the new requested state */ private void setExpandedInternal(boolean shouldExpand) { - if (DEBUG) { + if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand); } if (mExpanded == shouldExpand) { @@ -396,7 +465,7 @@ public class BubbleData { // bubble remains on top. mBubbles.remove(mSelectedBubble); mBubbles.add(0, mSelectedBubble); - packGroup(0); + mStateChange.orderChanged |= packGroup(0); } } } @@ -466,7 +535,7 @@ public class BubbleData { * @return true if the position of any bubbles has changed as a result */ private boolean packGroup(int position) { - if (DEBUG) { + if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "packGroup: position=" + position); } Bubble groupStart = mBubbles.get(position); @@ -495,7 +564,7 @@ public class BubbleData { * @return true if the position of any bubbles changed as a result */ private boolean repackAll() { - if (DEBUG) { + if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "repackAll()"); } if (mBubbles.isEmpty()) { @@ -550,28 +619,6 @@ public class BubbleData { } } - private void onBubbleBlocked(NotificationEntry entry) { - final String blockedGroupId = Bubble.groupId(entry); - int selectedIndex = mBubbles.indexOf(mSelectedBubble); - for (Iterator<Bubble> i = mBubbles.iterator(); i.hasNext(); ) { - Bubble bubble = i.next(); - if (bubble.getGroupId().equals(blockedGroupId)) { - mStateChange.bubbleRemoved(bubble, BubbleController.DISMISS_BLOCKED); - i.remove(); - } - } - if (mBubbles.isEmpty()) { - setExpandedInternal(false); - setSelectedBubbleInternal(null); - } else if (!mBubbles.contains(mSelectedBubble)) { - // choose a new one - int newIndex = Math.min(selectedIndex, mBubbles.size() - 1); - Bubble newSelected = mBubbles.get(newIndex); - setSelectedBubbleInternal(newSelected); - } - dispatchPendingChanges(); - } - private int indexForKey(String key) { for (int i = 0; i < mBubbles.size(); i++) { Bubble bubble = mBubbles.get(i); @@ -610,9 +657,21 @@ public class BubbleData { mListener = listener; } - boolean shouldAutoExpand(NotificationEntry entry) { - Notification.BubbleMetadata metadata = entry.getBubbleMetadata(); - return metadata != null && metadata.getAutoExpandBubble() - && BubbleController.isForegroundApp(mContext, entry.notification.getPackageName()); + /** + * Description of current bubble data state. + */ + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.print("selected: "); pw.println(mSelectedBubble != null + ? mSelectedBubble.getKey() + : "null"); + pw.print("expanded: "); pw.println(mExpanded); + pw.print("count: "); pw.println(mBubbles.size()); + for (Bubble bubble : mBubbles) { + bubble.dump(fd, pw, args); + } + pw.print("summaryKeys: "); pw.println(mSuppressedGroupKeys.size()); + for (String key : mSuppressedGroupKeys.keySet()) { + pw.println(" suppressing: " + key); + } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java new file mode 100644 index 000000000000..b702d06ca7cd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 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.bubbles; + +/** + * Common class for the various debug {@link android.util.Log} output configuration in the Bubbles + * package. + */ +public class BubbleDebugConfig { + + // All output logs in the Bubbles package use the {@link #TAG_BUBBLES} string for tagging their + // log output. This makes it easy to identify the origin of the log message when sifting + // through a large amount of log output from multiple sources. However, it also makes trying + // to figure-out the origin of a log message while debugging the Bubbles a little painful. By + // setting this constant to true, log messages from the Bubbles package will be tagged with + // their class names instead fot the generic tag. + static final boolean TAG_WITH_CLASS_NAME = false; + + // Default log tag for the Bubbles package. + static final String TAG_BUBBLES = "Bubbles"; + + static final boolean DEBUG_BUBBLE_CONTROLLER = false; + static final boolean DEBUG_BUBBLE_DATA = false; + static final boolean DEBUG_BUBBLE_STACK_VIEW = false; + static final boolean DEBUG_BUBBLE_EXPANDED_VIEW = false; + +} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDismissView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDismissView.java index 4db1e276f431..9db371e487c7 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDismissView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDismissView.java @@ -16,18 +16,13 @@ package com.android.systemui.bubbles; -import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; - import android.content.Context; -import android.graphics.drawable.Drawable; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.TextView; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringAnimation; @@ -37,14 +32,13 @@ import com.android.systemui.R; /** Dismiss view that contains a scrim gradient, as well as a dismiss icon, text, and circle. */ public class BubbleDismissView extends FrameLayout { - /** Duration for animations involving the dismiss target text/icon/gradient. */ + /** Duration for animations involving the dismiss target text/icon. */ private static final int DISMISS_TARGET_ANIMATION_BASE_DURATION = 150; - - private View mDismissGradient; + private static final float SCALE_FOR_POP = 1.2f; + private static final float SCALE_FOR_DISMISS = 0.9f; private LinearLayout mDismissTarget; private ImageView mDismissIcon; - private TextView mDismissText; private View mDismissCircle; private SpringAnimation mDismissTargetAlphaSpring; @@ -54,36 +48,15 @@ public class BubbleDismissView extends FrameLayout { super(context); setVisibility(GONE); - mDismissGradient = new FrameLayout(mContext); - - FrameLayout.LayoutParams gradientParams = - new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT); - gradientParams.gravity = Gravity.BOTTOM; - mDismissGradient.setLayoutParams(gradientParams); - - Drawable gradient = mContext.getResources().getDrawable(R.drawable.pip_dismiss_scrim); - gradient.setAlpha((int) (255 * 0.85f)); - mDismissGradient.setBackground(gradient); - - mDismissGradient.setVisibility(GONE); - addView(mDismissGradient); - LayoutInflater.from(context).inflate(R.layout.bubble_dismiss_target, this, true); mDismissTarget = findViewById(R.id.bubble_dismiss_icon_container); mDismissIcon = findViewById(R.id.bubble_dismiss_close_icon); - mDismissText = findViewById(R.id.bubble_dismiss_text); mDismissCircle = findViewById(R.id.bubble_dismiss_circle); // Set up the basic target area animations. These are very simple animations that don't need // fancy interpolators. final AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator(); - mDismissGradient.animate() - .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION) - .setInterpolator(interpolator); - mDismissText.animate() - .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION) - .setInterpolator(interpolator); mDismissIcon.animate() .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION) .setInterpolator(interpolator); @@ -108,110 +81,58 @@ public class BubbleDismissView extends FrameLayout { // safely assume it was animating out rather than in. if (alpha < 0.5f) { // If the alpha spring was animating the view out, set it to GONE when it's done. - setVisibility(GONE); + setVisibility(INVISIBLE); } }); } - /** Springs in the dismiss target and fades in the gradient. */ + /** Springs in the dismiss target. */ void springIn() { setVisibility(View.VISIBLE); - // Fade in the dismiss target (icon + text). + // Fade in the dismiss target icon. + mDismissIcon.animate() + .setDuration(50) + .scaleX(1f) + .scaleY(1f) + .alpha(1f); mDismissTarget.setAlpha(0f); mDismissTargetAlphaSpring.animateToFinalPosition(1f); - // Spring up the dismiss target (icon + text). + // Spring up the dismiss target. mDismissTarget.setTranslationY(mDismissTarget.getHeight() / 2f); mDismissTargetVerticalSpring.animateToFinalPosition(0); - // Fade in the gradient. - mDismissGradient.setVisibility(VISIBLE); - mDismissGradient.animate().alpha(1f); - - // Make sure the dismiss elements are in the separated position (in case we hid the target - // while they were condensed to cover the bubbles being in the target). - mDismissIcon.setAlpha(1f); - mDismissIcon.setScaleX(1f); - mDismissIcon.setScaleY(1f); - mDismissIcon.setTranslationX(0f); - mDismissText.setAlpha(1f); - mDismissText.setTranslationX(0f); + mDismissCircle.setAlpha(0f); + mDismissCircle.setScaleX(SCALE_FOR_POP); + mDismissCircle.setScaleY(SCALE_FOR_POP); + + // Fade in circle and reduce size. + mDismissCircle.animate() + .alpha(1f) + .scaleX(1f) + .scaleY(1f); } - /** Springs out the dismiss target and fades out the gradient. */ + /** Springs out the dismiss target. */ void springOut() { + // Fade out the target icon. + mDismissIcon.animate() + .setDuration(50) + .scaleX(SCALE_FOR_DISMISS) + .scaleY(SCALE_FOR_DISMISS) + .alpha(0f); + // Fade out the target. mDismissTargetAlphaSpring.animateToFinalPosition(0f); // Spring the target down a bit. mDismissTargetVerticalSpring.animateToFinalPosition(mDismissTarget.getHeight() / 2f); - // Fade out the gradient and then set it to GONE so it's not in the SBV hierarchy. - mDismissGradient.animate().alpha(0f).withEndAction( - () -> mDismissGradient.setVisibility(GONE)); - - // Pop out the dismiss circle. - mDismissCircle.animate().alpha(0f).scaleX(1.2f).scaleY(1.2f); - } - - /** - * Encircles the center of the dismiss target, pulling the X towards the center and hiding the - * text. - */ - void animateEncircleCenterWithX(boolean encircle) { - // Pull the text towards the center if we're encircling (it'll be faded out, leaving only - // the X icon over the bubbles), or back to normal if we're un-encircling. - final float textTranslation = encircle - ? -mDismissIcon.getWidth() / 4f - : 0f; - - // Center the icon if we're encircling, or put it back to normal if not. - final float iconTranslation = encircle - ? mDismissTarget.getWidth() / 2f - - mDismissIcon.getWidth() / 2f - - mDismissIcon.getLeft() - : 0f; - - // Fade in/out the text and translate it. - mDismissText.animate() - .alpha(encircle ? 0f : 1f) - .translationX(textTranslation); - - mDismissIcon.animate() - .setDuration(150) - .translationX(iconTranslation); - - // Fade out the gradient if we're encircling (the bubbles will 'absorb' it by darkening - // themselves). - mDismissGradient.animate() - .alpha(encircle ? 0f : 1f); - - // Prepare the circle to be 'dropped in'. - if (encircle) { - mDismissCircle.setAlpha(0f); - mDismissCircle.setScaleX(1.2f); - mDismissCircle.setScaleY(1.2f); - } - - // Drop in the circle, or pull it back up. - mDismissCircle.animate() - .alpha(encircle ? 1f : 0f) - .scaleX(encircle ? 1f : 0f) - .scaleY(encircle ? 1f : 0f); - } - - /** Animates the circle and the centered icon out. */ - void animateEncirclingCircleDisappearance() { - // Pop out the dismiss icon and circle. - mDismissIcon.animate() - .setDuration(50) - .scaleX(0.9f) - .scaleY(0.9f) - .alpha(0f); + // Pop out the circle. mDismissCircle.animate() - .scaleX(0.9f) - .scaleY(0.9f) + .scaleX(SCALE_FOR_DISMISS) + .scaleY(SCALE_FOR_DISMISS) .alpha(0f); } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java index de08a8c9c1b3..be10dc565159 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java @@ -18,14 +18,15 @@ package com.android.systemui.bubbles; import static android.view.Display.INVALID_DISPLAY; -import static com.android.systemui.bubbles.BubbleController.DEBUG_ENABLE_AUTO_BUBBLE; +import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW; +import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; -import android.annotation.Nullable; import android.app.ActivityOptions; +import android.app.ActivityTaskManager; import android.app.ActivityView; -import android.app.INotificationManager; -import android.app.Notification; import android.app.PendingIntent; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -35,18 +36,17 @@ import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Insets; import android.graphics.Point; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; -import android.os.ServiceManager; -import android.os.UserHandle; -import android.provider.Settings; +import android.os.RemoteException; import android.service.notification.StatusBarNotification; import android.util.AttributeSet; import android.util.Log; import android.util.StatsLog; import android.view.View; -import android.view.ViewGroup; import android.view.WindowInsets; +import android.view.WindowManager; import android.widget.LinearLayout; import com.android.internal.policy.ScreenDecorationsUtils; @@ -54,15 +54,23 @@ import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.recents.TriangleShape; import com.android.systemui.statusbar.AlphaOptimizedButton; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; -import com.android.systemui.statusbar.notification.stack.ExpandableViewState; /** * Container for the expanded bubble view, handles rendering the caret and settings icon. */ public class BubbleExpandedView extends LinearLayout implements View.OnClickListener { - private static final String TAG = "BubbleExpandedView"; + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES; + + private enum ActivityViewStatus { + // ActivityView is being initialized, cannot start an activity yet. + INITIALIZING, + // ActivityView is initialized, and ready to start an activity. + INITIALIZED, + // Activity runs in the ActivityView. + ACTIVITY_STARTED, + // ActivityView is released, so activity launching will no longer be permitted. + RELEASED, + } // The triangle pointing to the expanded view private View mPointerView; @@ -71,50 +79,89 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList private AlphaOptimizedButton mSettingsIcon; // Views for expanded state - private ExpandableNotificationRow mNotifRow; private ActivityView mActivityView; - private boolean mActivityViewReady = false; + private ActivityViewStatus mActivityViewStatus = ActivityViewStatus.INITIALIZING; + private int mTaskId = -1; + private PendingIntent mBubbleIntent; private boolean mKeyboardVisible; private boolean mNeedsNewHeight; + private Point mDisplaySize; private int mMinHeight; private int mSettingsIconHeight; - private int mBubbleHeight; private int mPointerWidth; private int mPointerHeight; private ShapeDrawable mPointerDrawable; + private Rect mTempRect = new Rect(); + private int[] mTempLoc = new int[2]; + private int mExpandedViewTouchSlop; - private NotificationEntry mEntry; + private Bubble mBubble; private PackageManager mPm; private String mAppName; private Drawable mAppIcon; - private INotificationManager mNotificationManagerService; private BubbleController mBubbleController = Dependency.get(BubbleController.class); private BubbleStackView mStackView; - private BubbleExpandedView.OnBubbleBlockedListener mOnBubbleBlockedListener; - private ActivityView.StateCallback mStateCallback = new ActivityView.StateCallback() { @Override public void onActivityViewReady(ActivityView view) { - if (!mActivityViewReady) { - mActivityViewReady = true; - // Custom options so there is no activity transition animation - ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(), - 0 /* enterResId */, 0 /* exitResId */); - // Post to keep the lifecycle normal - post(() -> mActivityView.startActivity(mBubbleIntent, options)); + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onActivityViewReady: mActivityViewStatus=" + mActivityViewStatus + + " bubble=" + getBubbleKey()); + } + switch (mActivityViewStatus) { + case INITIALIZING: + case INITIALIZED: + // Custom options so there is no activity transition animation + ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(), + 0 /* enterResId */, 0 /* exitResId */); + // Post to keep the lifecycle normal + post(() -> { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onActivityViewReady: calling startActivity, " + + "bubble=" + getBubbleKey()); + } + try { + mActivityView.startActivity(mBubbleIntent, options); + } catch (RuntimeException e) { + // If there's a runtime exception here then there's something + // wrong with the intent, we can't really recover / try to populate + // the bubble again so we'll just remove it. + Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() + + ", " + e.getMessage() + "; removing bubble"); + mBubbleController.removeBubble(mBubble.getKey(), + BubbleController.DISMISS_INVALID_INTENT); + } + }); + mActivityViewStatus = ActivityViewStatus.ACTIVITY_STARTED; } } @Override public void onActivityViewDestroyed(ActivityView view) { - mActivityViewReady = false; + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onActivityViewDestroyed: mActivityViewStatus=" + mActivityViewStatus + + " bubble=" + getBubbleKey()); + } + mActivityViewStatus = ActivityViewStatus.RELEASED; + } + + @Override + public void onTaskCreated(int taskId, ComponentName componentName) { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onTaskCreated: taskId=" + taskId + + " bubble=" + getBubbleKey()); + } + // Since Bubble ActivityView applies singleTaskDisplay this is + // guaranteed to only be called once per ActivityView. The taskId is + // saved to use for removeTask, preventing appearance in recent tasks. + mTaskId = taskId; } /** @@ -125,9 +172,14 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList */ @Override public void onTaskRemovalStarted(int taskId) { - if (mEntry != null) { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId + + " mActivityViewStatus=" + mActivityViewStatus + + " bubble=" + getBubbleKey()); + } + if (mBubble != null) { // Must post because this is called from a binder thread. - post(() -> mBubbleController.removeBubble(mEntry.key, + post(() -> mBubbleController.removeBubble(mBubble.getKey(), BubbleController.DISMISS_TASK_FINISHED)); } } @@ -149,20 +201,22 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mPm = context.getPackageManager(); - mMinHeight = getResources().getDimensionPixelSize( - R.dimen.bubble_expanded_default_height); - mPointerMargin = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_margin); - try { - mNotificationManagerService = INotificationManager.Stub.asInterface( - ServiceManager.getServiceOrThrow(Context.NOTIFICATION_SERVICE)); - } catch (ServiceManager.ServiceNotFoundException e) { - Log.w(TAG, e); - } + mDisplaySize = new Point(); + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + // Get the real size -- this includes screen decorations (notches, statusbar, navbar). + wm.getDefaultDisplay().getRealSize(mDisplaySize); + Resources res = getResources(); + mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height); + mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin); + mExpandedViewTouchSlop = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_slop); } @Override protected void onFinishInflate() { super.onFinishInflate(); + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onFinishInflate: bubble=" + getBubbleKey()); + } Resources res = getResources(); mPointerView = findViewById(R.id.pointer_view); @@ -173,10 +227,10 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList mPointerDrawable = new ShapeDrawable(TriangleShape.create( mPointerWidth, mPointerHeight, true /* pointUp */)); mPointerView.setBackground(mPointerDrawable); - mPointerView.setVisibility(GONE); + mPointerView.setVisibility(INVISIBLE); mSettingsIconHeight = getContext().getResources().getDimensionPixelSize( - R.dimen.bubble_expanded_header_height); + R.dimen.bubble_settings_size); mSettingsIcon = findViewById(R.id.settings_button); mSettingsIcon.setOnClickListener(this); @@ -210,6 +264,10 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList }); } + private String getBubbleKey() { + return mBubble != null ? mBubble.getKey() : "null"; + } + void applyThemeAttrs() { TypedArray ta = getContext().obtainStyledAttributes(R.styleable.BubbleExpandedView); int bgColor = ta.getColor( @@ -235,6 +293,9 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList if (mActivityView != null) { mActivityView.setForwardedInsets(Insets.of(0, 0, 0, 0)); } + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onDetachedFromWindow: bubble=" + getBubbleKey()); + } } /** @@ -246,6 +307,10 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList * and setting {@code false} actually means rendering the contents in transparent. */ void setContentVisibility(boolean visibility) { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "setContentVisibility: visibility=" + visibility + + " bubble=" + getBubbleKey()); + } final float alpha = visibility ? 1f : 0f; mPointerView.setAlpha(alpha); if (mActivityView != null) { @@ -259,44 +324,34 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList */ void updateInsets(WindowInsets insets) { if (usingActivityView()) { - Point displaySize = new Point(); - mActivityView.getContext().getDisplay().getSize(displaySize); - int[] windowLocation = mActivityView.getLocationOnScreen(); - final int windowBottom = windowLocation[1] + mActivityView.getHeight(); - final int keyboardHeight = insets.getSystemWindowInsetBottom() - - insets.getStableInsetBottom(); - final int insetsBottom = Math.max(0, - windowBottom + keyboardHeight - displaySize.y); + int[] screenLoc = mActivityView.getLocationOnScreen(); + final int activityViewBottom = screenLoc[1] + mActivityView.getHeight(); + final int keyboardTop = mDisplaySize.y - insets.getSystemWindowInsetBottom(); + final int insetsBottom = Math.max(activityViewBottom - keyboardTop, 0); mActivityView.setForwardedInsets(Insets.of(0, 0, 0, insetsBottom)); } } /** - * Sets the listener to notify when a bubble has been blocked. + * Sets the bubble used to populate this view. */ - public void setOnBlockedListener(OnBubbleBlockedListener listener) { - mOnBubbleBlockedListener = listener; - } + public void setBubble(Bubble bubble, BubbleStackView stackView, String appName) { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "setBubble: bubble=" + (bubble != null ? bubble.getKey() : "null")); + } - /** - * Sets the notification entry used to populate this view. - */ - public void setEntry(NotificationEntry entry, BubbleStackView stackView, String appName) { mStackView = stackView; - mEntry = entry; + mBubble = bubble; mAppName = appName; - ApplicationInfo info; try { - info = mPm.getApplicationInfo( - entry.notification.getPackageName(), + ApplicationInfo info = mPm.getApplicationInfo( + bubble.getPackageName(), PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_DIRECT_BOOT_AWARE); - if (info != null) { - mAppIcon = mPm.getApplicationIcon(info); - } + mAppIcon = mPm.getApplicationIcon(info); } catch (PackageManager.NameNotFoundException e) { // Do nothing. } @@ -312,52 +367,46 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList * Lets activity view know it should be shown / populated. */ public void populateExpandedView() { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "populateExpandedView: " + + "bubble=" + getBubbleKey()); + } + if (usingActivityView()) { mActivityView.setCallback(mStateCallback); } else { - // We're using notification template - ViewGroup parent = (ViewGroup) mNotifRow.getParent(); - if (parent == this) { - // Already added - return; - } else if (parent != null) { - // Still in the shade... remove it - parent.removeView(mNotifRow); - } - addView(mNotifRow, 1 /* index */); - mPointerView.setAlpha(1f); + Log.e(TAG, "Cannot populate expanded view."); } } /** - * Updates the entry backing this view. This will not re-populate ActivityView, it will + * Updates the bubble backing this view. This will not re-populate ActivityView, it will * only update the deep-links in the title, and the height of the view. */ - public void update(NotificationEntry entry) { - if (entry.key.equals(mEntry.key)) { - mEntry = entry; + public void update(Bubble bubble) { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "update: bubble=" + (bubble != null ? bubble.getKey() : "null")); + } + if (bubble.getKey().equals(mBubble.getKey())) { + mBubble = bubble; updateSettingsContentDescription(); updateHeight(); } else { - Log.w(TAG, "Trying to update entry with different key, new entry: " - + entry.key + " old entry: " + mEntry.key); + Log.w(TAG, "Trying to update entry with different key, new bubble: " + + bubble.getKey() + " old bubble: " + bubble.getKey()); } } private void updateExpandedView() { - mBubbleIntent = getBubbleIntent(mEntry); + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "updateExpandedView: bubble=" + + getBubbleKey()); + } + + mBubbleIntent = mBubble.getBubbleIntent(mContext); if (mBubbleIntent != null) { - if (mNotifRow != null) { - // Clear out the row if we had it previously - removeView(mNotifRow); - mNotifRow = null; - } setContentVisibility(false); mActivityView.setVisibility(VISIBLE); - } else if (DEBUG_ENABLE_AUTO_BUBBLE) { - // Hide activity view if we had it previously - mActivityView.setVisibility(GONE); - mNotifRow = mEntry.getRow(); } updateView(); } @@ -371,29 +420,12 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList } void updateHeight() { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "updateHeight: bubble=" + getBubbleKey()); + } if (usingActivityView()) { - Notification.BubbleMetadata data = mEntry.getBubbleMetadata(); - float desiredHeight; - if (data == null) { - // This is a contentIntent based bubble, lets allow it to be the max height - // as it was forced into this mode and not prepared to be small - desiredHeight = mStackView.getMaxExpandedHeight(); - } else { - boolean useRes = data.getDesiredHeightResId() != 0; - float desiredPx; - if (useRes) { - desiredPx = getDimenForPackageUser(data.getDesiredHeightResId(), - mEntry.notification.getPackageName(), - mEntry.notification.getUser().getIdentifier()); - } else { - desiredPx = data.getDesiredHeight() - * getContext().getResources().getDisplayMetrics().density; - } - desiredHeight = desiredPx > 0 ? desiredPx : mMinHeight; - } - int max = mStackView.getMaxExpandedHeight() - mSettingsIconHeight - mPointerHeight - - mPointerMargin; - float height = Math.min(desiredHeight, max); + float desiredHeight = Math.max(mBubble.getDesiredHeight(mContext), mMinHeight); + float height = Math.min(desiredHeight, getMaxExpandedHeight()); height = Math.max(height, mMinHeight); LayoutParams lp = (LayoutParams) mActivityView.getLayoutParams(); mNeedsNewHeight = lp.height != height; @@ -401,28 +433,65 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList // If the keyboard is visible... don't adjust the height because that will cause // a configuration change and the keyboard will be lost. lp.height = (int) height; - mBubbleHeight = (int) height; mActivityView.setLayoutParams(lp); mNeedsNewHeight = false; } - } else { - mBubbleHeight = mNotifRow != null ? mNotifRow.getIntrinsicHeight() : mMinHeight; + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "updateHeight: bubble=" + getBubbleKey() + " height=" + height + + " mNeedsNewHeight=" + mNeedsNewHeight); + } + } + } + + private int getMaxExpandedHeight() { + int[] windowLocation = mActivityView.getLocationOnScreen(); + int bottomInset = getRootWindowInsets() != null + ? getRootWindowInsets().getStableInsetBottom() + : 0; + return mDisplaySize.y - windowLocation[1] - mSettingsIconHeight - mPointerHeight + - mPointerMargin - bottomInset; + } + + /** + * Whether the provided x, y values (in raw coordinates) are in a touchable area of the + * expanded view. + * + * The touchable areas are the ActivityView (plus some slop around it) and the manage button. + */ + boolean intersectingTouchableContent(int rawX, int rawY) { + mTempRect.setEmpty(); + if (mActivityView != null) { + mTempLoc = mActivityView.getLocationOnScreen(); + mTempRect.set(mTempLoc[0] - mExpandedViewTouchSlop, + mTempLoc[1] - mExpandedViewTouchSlop, + mTempLoc[0] + mActivityView.getWidth() + mExpandedViewTouchSlop, + mTempLoc[1] + mActivityView.getHeight() + mExpandedViewTouchSlop); + } + if (mTempRect.contains(rawX, rawY)) { + return true; + } + mTempLoc = mSettingsIcon.getLocationOnScreen(); + mTempRect.set(mTempLoc[0], + mTempLoc[1], + mTempLoc[0] + mSettingsIcon.getWidth(), + mTempLoc[1] + mSettingsIcon.getHeight()); + if (mTempRect.contains(rawX, rawY)) { + return true; } + return false; } @Override public void onClick(View view) { - if (mEntry == null) { + if (mBubble == null) { return; } - Notification n = mEntry.notification.getNotification(); int id = view.getId(); if (id == R.id.settings_button) { - Intent intent = getSettingsIntent(mEntry.notification.getPackageName(), - mEntry.notification.getUid()); + Intent intent = mBubble.getSettingsIntent(); mStackView.collapseStack(() -> { - mContext.startActivityAsUser(intent, mEntry.notification.getUser()); - logBubbleClickEvent(mEntry, + mContext.startActivityAsUser(intent, mBubble.getEntry().notification.getUser()); + logBubbleClickEvent(mBubble, StatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS); }); } @@ -442,13 +511,14 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList * Update appearance of the expanded view being displayed. */ public void updateView() { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "updateView: bubble=" + + getBubbleKey()); + } if (usingActivityView() && mActivityView.getVisibility() == VISIBLE && mActivityView.isAttachedToWindow()) { mActivityView.onLocationChanged(); - } else if (mNotifRow != null) { - applyRowState(mNotifRow); - mPointerView.setAlpha(1f); } updateHeight(); } @@ -467,17 +537,44 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList * Removes and releases an ActivityView if one was previously created for this bubble. */ public void cleanUpExpandedState() { - removeView(mNotifRow); - + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "cleanUpExpandedState: mActivityViewStatus=" + mActivityViewStatus + + ", bubble=" + getBubbleKey()); + } if (mActivityView == null) { return; } - if (mActivityViewReady) { - mActivityView.release(); + switch (mActivityViewStatus) { + case INITIALIZED: + case ACTIVITY_STARTED: + mActivityView.release(); + } + if (mTaskId != -1) { + try { + ActivityTaskManager.getService().removeTask(mTaskId); + } catch (RemoteException e) { + Log.w(TAG, "Failed to remove taskId " + mTaskId); + } + mTaskId = -1; } removeView(mActivityView); + mActivityView = null; - mActivityViewReady = false; + } + + /** + * Called when the last task is removed from a {@link android.hardware.display.VirtualDisplay} + * which {@link ActivityView} uses. + */ + void notifyDisplayEmpty() { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "notifyDisplayEmpty: bubble=" + + getBubbleKey() + + " mActivityViewStatus=" + mActivityViewStatus); + } + if (mActivityViewStatus == ActivityViewStatus.ACTIVITY_STARTED) { + mActivityViewStatus = ActivityViewStatus.INITIALIZED; + } } private boolean usingActivityView() { @@ -494,76 +591,14 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList return INVALID_DISPLAY; } - private void applyRowState(ExpandableNotificationRow view) { - view.reset(); - view.setHeadsUp(false); - view.resetTranslation(); - view.setOnKeyguard(false); - view.setClipBottomAmount(0); - view.setClipTopAmount(0); - view.setContentTransformationAmount(0, false); - view.setIconsVisible(true); - - // TODO - Need to reset this (and others) when view goes back in shade, leave for now - // view.setTopRoundness(1, false); - // view.setBottomRoundness(1, false); - - ExpandableViewState viewState = view.getViewState(); - viewState = viewState == null ? new ExpandableViewState() : viewState; - viewState.height = view.getIntrinsicHeight(); - viewState.gone = false; - viewState.hidden = false; - viewState.dimmed = false; - viewState.alpha = 1f; - viewState.notGoneIndex = -1; - viewState.xTranslation = 0; - viewState.yTranslation = 0; - viewState.zTranslation = 0; - viewState.scaleX = 1; - viewState.scaleY = 1; - viewState.inShelf = true; - viewState.headsUpIsVisible = false; - viewState.applyToView(view); - } - - private Intent getSettingsIntent(String packageName, final int appUid) { - final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS); - intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName); - intent.putExtra(Settings.EXTRA_APP_UID, appUid); - intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - return intent; - } - - @Nullable - private PendingIntent getBubbleIntent(NotificationEntry entry) { - Notification notif = entry.notification.getNotification(); - Notification.BubbleMetadata data = notif.getBubbleMetadata(); - if (BubbleController.canLaunchInActivityView(mContext, entry) && data != null) { - return data.getIntent(); - } - return null; - } - - /** - * Listener that is notified when a bubble is blocked. - */ - public interface OnBubbleBlockedListener { - /** - * Called when a bubble is blocked for the provided entry. - */ - void onBubbleBlocked(NotificationEntry entry); - } - /** * Logs bubble UI click event. * - * @param entry the bubble notification entry that user is interacting with. + * @param bubble the bubble notification entry that user is interacting with. * @param action the user interaction enum. */ - private void logBubbleClickEvent(NotificationEntry entry, int action) { - StatusBarNotification notification = entry.notification; + private void logBubbleClickEvent(Bubble bubble, int action) { + StatusBarNotification notification = bubble.getEntry().notification; StatsLog.write(StatsLog.BUBBLE_UI_CHANGED, notification.getPackageName(), notification.getNotification().getChannelId(), @@ -573,27 +608,8 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList action, mStackView.getNormalizedXPosition(), mStackView.getNormalizedYPosition(), - entry.showInShadeWhenBubble(), - entry.isForegroundService(), - BubbleController.isForegroundApp(mContext, notification.getPackageName())); - } - - private int getDimenForPackageUser(int resId, String pkg, int userId) { - Resources r; - if (pkg != null) { - try { - if (userId == UserHandle.USER_ALL) { - userId = UserHandle.USER_SYSTEM; - } - r = mPm.getResourcesForApplicationAsUser(pkg, userId); - return r.getDimensionPixelSize(resId); - } catch (PackageManager.NameNotFoundException ex) { - // Uninstalled, don't care - } catch (Resources.NotFoundException e) { - // Invalid res id, return 0 and user our default - Log.e(TAG, "Couldn't find desired height res id", e); - } - } - return 0; + bubble.showInShadeWhenBubble(), + bubble.isOngoing(), + false /* isAppForeground (unused) */); } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java index 71f68c16bd8d..58f3f2211d81 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java @@ -39,8 +39,7 @@ import android.view.ViewOutlineProvider; import android.widget.FrameLayout; import android.widget.TextView; -import androidx.dynamicanimation.animation.DynamicAnimation; -import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.annotation.Nullable; import com.android.systemui.R; import com.android.systemui.recents.TriangleShape; @@ -57,6 +56,9 @@ public class BubbleFlyoutView extends FrameLayout { private final int mFlyoutSpaceFromBubble; private final int mPointerSize; private final int mBubbleSize; + private final int mBubbleIconBitmapSize; + private final float mBubbleIconTopPadding; + private final int mFlyoutElevation; private final int mBubbleElevation; private final int mFloatingBackgroundColor; @@ -64,14 +66,11 @@ public class BubbleFlyoutView extends FrameLayout { private final ViewGroup mFlyoutTextContainer; private final TextView mFlyoutText; - /** Spring animation for the flyout. */ - private final SpringAnimation mFlyoutSpring = - new SpringAnimation(this, DynamicAnimation.TRANSLATION_X); /** Values related to the 'new' dot which we use to figure out where to collapse the flyout. */ private final float mNewDotRadius; private final float mNewDotSize; - private final float mNewDotOffsetFromBubbleBounds; + private final float mOriginalDotSize; /** * The paint used to draw the background, whose color changes as the flyout transitions to the @@ -113,7 +112,6 @@ public class BubbleFlyoutView extends FrameLayout { */ private float mFlyoutToDotWidthDelta = 0f; private float mFlyoutToDotHeightDelta = 0f; - private float mFlyoutToDotCornerRadiusDelta; /** The translation values when the flyout is completely transitioned into the dot. */ private float mTranslationXWhenDot = 0f; @@ -126,11 +124,18 @@ public class BubbleFlyoutView extends FrameLayout { private float mBgTranslationX; private float mBgTranslationY; + private float[] mDotCenter; + /** The flyout's X translation when at rest (not animating or dragging). */ private float mRestingTranslationX = 0f; + /** The badge sizes are defined as percentages of the app icon size. Same value as Launcher3. */ + private static final float SIZE_PERCENTAGE = 0.228f; + + private static final float DOT_SCALE = 1f; + /** Callback to run when the flyout is hidden. */ - private Runnable mOnHide; + @Nullable private Runnable mOnHide; public BubbleFlyoutView(Context context) { super(context); @@ -143,11 +148,16 @@ public class BubbleFlyoutView extends FrameLayout { mFlyoutPadding = res.getDimensionPixelSize(R.dimen.bubble_flyout_padding_x); mFlyoutSpaceFromBubble = res.getDimensionPixelSize(R.dimen.bubble_flyout_space_from_bubble); mPointerSize = res.getDimensionPixelSize(R.dimen.bubble_flyout_pointer_size); + mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); + mBubbleIconBitmapSize = res.getDimensionPixelSize(R.dimen.bubble_icon_bitmap_size); + mBubbleIconTopPadding = (mBubbleSize - mBubbleIconBitmapSize) / 2f; + mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); mFlyoutElevation = res.getDimensionPixelSize(R.dimen.bubble_flyout_elevation); - mNewDotOffsetFromBubbleBounds = BadgeRenderer.getDotCenterOffset(context); - mNewDotRadius = BadgeRenderer.getDotRadius(mNewDotOffsetFromBubbleBounds); + + mOriginalDotSize = SIZE_PERCENTAGE * mBubbleIconBitmapSize; + mNewDotRadius = (DOT_SCALE * mOriginalDotSize) / 2f; mNewDotSize = mNewDotRadius * 2f; final TypedArray ta = mContext.obtainStyledAttributes( @@ -156,7 +166,6 @@ public class BubbleFlyoutView extends FrameLayout { android.R.attr.dialogCornerRadius}); mFloatingBackgroundColor = ta.getColor(0, Color.WHITE); mCornerRadius = ta.getDimensionPixelSize(1, 0); - mFlyoutToDotCornerRadiusDelta = mNewDotRadius - mCornerRadius; ta.recycle(); // Add padding for the pointer on either side, onDraw will draw it in this space. @@ -193,17 +202,17 @@ public class BubbleFlyoutView extends FrameLayout { super.onDraw(canvas); } - /** Configures the flyout and animates it in. */ - void showFlyout( + /** Configures the flyout, collapsed into to dot form. */ + void setupFlyoutStartingAsDot( CharSequence updateMessage, PointF stackPos, float parentWidth, - boolean arrowPointingLeft, int dotColor, Runnable onHide) { + boolean arrowPointingLeft, int dotColor, @Nullable Runnable onLayoutComplete, + @Nullable Runnable onHide, float[] dotCenter) { mArrowPointingLeft = arrowPointingLeft; mDotColor = dotColor; mOnHide = onHide; + mDotCenter = dotCenter; - setCollapsePercent(0f); - setAlpha(0f); - setVisibility(VISIBLE); + setCollapsePercent(1f); // Set the flyout TextView's max width in terms of percent, and then subtract out the // padding so that the entire flyout view will be the desired width (rather than the @@ -214,14 +223,16 @@ public class BubbleFlyoutView extends FrameLayout { // Wait for the TextView to lay out so we know its line count. post(() -> { + float restingTranslationY; // Multi line flyouts get top-aligned to the bubble. if (mFlyoutText.getLineCount() > 1) { - setTranslationY(stackPos.y); + restingTranslationY = stackPos.y + mBubbleIconTopPadding; } else { // Single line flyouts are vertically centered with respect to the bubble. - setTranslationY( - stackPos.y + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f); + restingTranslationY = + stackPos.y + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f; } + setTranslationY(restingTranslationY); // Calculate the translation required to position the flyout next to the bubble stack, // with the desired padding. @@ -229,40 +240,30 @@ public class BubbleFlyoutView extends FrameLayout { ? stackPos.x + mBubbleSize + mFlyoutSpaceFromBubble : stackPos.x - getWidth() - mFlyoutSpaceFromBubble; - // Translate towards the stack slightly. - setTranslationX( - mRestingTranslationX + (arrowPointingLeft ? -mBubbleSize : mBubbleSize)); - - // Fade in the entire flyout and spring it to its normal position. - animate().alpha(1f); - mFlyoutSpring.animateToFinalPosition(mRestingTranslationX); - // Calculate the difference in size between the flyout and the 'dot' so that we can // transform into the dot later. mFlyoutToDotWidthDelta = getWidth() - mNewDotSize; mFlyoutToDotHeightDelta = getHeight() - mNewDotSize; // Calculate the translation values needed to be in the correct 'new dot' position. - final float distanceFromFlyoutLeftToDotCenterX = - mFlyoutSpaceFromBubble + mNewDotOffsetFromBubbleBounds / 2; - if (mArrowPointingLeft) { - mTranslationXWhenDot = -distanceFromFlyoutLeftToDotCenterX - mNewDotRadius; - } else { - mTranslationXWhenDot = - getWidth() + distanceFromFlyoutLeftToDotCenterX - mNewDotRadius; - } + final float dotPositionX = stackPos.x + mDotCenter[0] - (mOriginalDotSize / 2f); + final float dotPositionY = stackPos.y + mDotCenter[1] - (mOriginalDotSize / 2f); + + final float distanceFromFlyoutLeftToDotCenterX = mRestingTranslationX - dotPositionX; + final float distanceFromLayoutTopToDotCenterY = restingTranslationY - dotPositionY; - mTranslationYWhenDot = - getHeight() / 2f - - mNewDotRadius - - mBubbleSize / 2f - + mNewDotOffsetFromBubbleBounds / 2; + mTranslationXWhenDot = -distanceFromFlyoutLeftToDotCenterX; + mTranslationYWhenDot = -distanceFromLayoutTopToDotCenterY; + if (onLayoutComplete != null) { + onLayoutComplete.run(); + } }); } /** - * Hides the flyout and runs the optional callback passed into showFlyout. The flyout has been - * animated into the 'new' dot by the time we call this, so no animations are needed. + * Hides the flyout and runs the optional callback passed into setupFlyoutStartingAsDot. + * The flyout has been animated into the 'new' dot by the time we call this, so no animations + * are needed. */ void hideFlyout() { if (mOnHide != null) { @@ -275,6 +276,13 @@ public class BubbleFlyoutView extends FrameLayout { /** Sets the percentage that the flyout should be collapsed into dot form. */ void setCollapsePercent(float percentCollapsed) { + // This is unlikely, but can happen in a race condition where the flyout view hasn't been + // laid out and returns 0 for getWidth(). We check for this condition at the sites where + // this method is called, but better safe than sorry. + if (Float.isNaN(percentCollapsed)) { + return; + } + mPercentTransitionedToDot = Math.max(0f, Math.min(percentCollapsed, 1f)); mPercentStillFlyout = (1f - mPercentTransitionedToDot); @@ -311,8 +319,8 @@ public class BubbleFlyoutView extends FrameLayout { // percentage. final float width = getWidth() - (mFlyoutToDotWidthDelta * mPercentTransitionedToDot); final float height = getHeight() - (mFlyoutToDotHeightDelta * mPercentTransitionedToDot); - final float cornerRadius = mCornerRadius - - (mFlyoutToDotCornerRadiusDelta * mPercentTransitionedToDot); + final float interpolatedRadius = mNewDotRadius * mPercentTransitionedToDot + + mCornerRadius * (1 - mPercentTransitionedToDot); // Translate the flyout background towards the collapsed 'dot' state. mBgTranslationX = mTranslationXWhenDot * mPercentTransitionedToDot; @@ -336,7 +344,7 @@ public class BubbleFlyoutView extends FrameLayout { canvas.save(); canvas.translate(mBgTranslationX, mBgTranslationY); renderPointerTriangle(canvas, width, height); - canvas.drawRoundRect(mBgRect, cornerRadius, cornerRadius, mBgPaint); + canvas.drawRoundRect(mBgRect, interpolatedRadius, interpolatedRadius, mBgPaint); canvas.restore(); } @@ -379,7 +387,10 @@ public class BubbleFlyoutView extends FrameLayout { if (!mTriangleOutline.isEmpty()) { // Draw the rect into the outline as a path so we can merge the triangle path into it. final Path rectPath = new Path(); - rectPath.addRoundRect(mBgRect, mCornerRadius, mCornerRadius, Path.Direction.CW); + final float interpolatedRadius = mNewDotRadius * mPercentTransitionedToDot + + mCornerRadius * (1 - mPercentTransitionedToDot); + rectPath.addRoundRect(mBgRect, interpolatedRadius, + interpolatedRadius, Path.Direction.CW); outline.setConvexPath(rectPath); // Get rid of the triangle path once it has disappeared behind the flyout. diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java new file mode 100644 index 000000000000..a1c77c0af6bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java @@ -0,0 +1,38 @@ +/* + * 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.bubbles; + +import android.content.Context; + +import com.android.launcher3.icons.BaseIconFactory; +import com.android.systemui.R; + +/** + * Factory for creating normalized bubble icons. + * We are not using Launcher's IconFactory because bubbles only runs on the UI thread, + * so there is no need to manage a pool across multiple threads. + */ +public class BubbleIconFactory extends BaseIconFactory { + protected BubbleIconFactory(Context context) { + super(context, context.getResources().getConfiguration().densityDpi, + context.getResources().getDimensionPixelSize(R.dimen.individual_bubble_size)); + } + + public int getBadgeSize() { + return mContext.getResources().getDimensionPixelSize( + com.android.launcher3.icons.R.dimen.profile_badge_size); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index f87bcef5fde2..13d6470a351f 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -19,16 +19,20 @@ package com.android.systemui.bubbles; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW; +import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.app.Notification; import android.content.Context; +import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; -import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Point; import android.graphics.PointF; @@ -45,7 +49,6 @@ import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; -import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.WindowManager; @@ -69,6 +72,8 @@ import com.android.systemui.bubbles.animation.PhysicsAnimationLayout; import com.android.systemui.bubbles.animation.StackAnimationController; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; @@ -79,8 +84,7 @@ import java.util.List; * Renders bubbles in a stack and handles animating expanded and collapsed states. */ public class BubbleStackView extends FrameLayout { - private static final String TAG = "BubbleStackView"; - private static final boolean DEBUG = false; + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES; /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */ static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f; @@ -160,9 +164,14 @@ public class BubbleStackView extends FrameLayout { private BubbleFlyoutView mFlyout; /** Runnable that fades out the flyout and then sets it to GONE. */ private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */); + /** + * Callback to run after the flyout hides. Also called if a new flyout is shown before the + * previous one animates out. + */ + private Runnable mAfterFlyoutHides; /** Layout change listener that moves the stack to the nearest valid position on rotation. */ - private OnLayoutChangeListener mMoveStackToValidPositionOnLayoutListener; + private OnLayoutChangeListener mOrientationChangedListener; /** Whether the stack was on the left side of the screen prior to rotation. */ private boolean mWasOnLeftBeforeRotation = false; /** @@ -172,18 +181,17 @@ public class BubbleStackView extends FrameLayout { private float mVerticalPosPercentBeforeRotation = -1; private int mBubbleSize; - private int mBubblePadding; + private int mBubblePaddingTop; + private int mBubbleTouchPadding; private int mExpandedViewPadding; private int mExpandedAnimateXDistance; private int mExpandedAnimateYDistance; private int mPointerHeight; private int mStatusBarHeight; - private int mPipDismissHeight; private int mImeOffset; - + private BubbleIconFactory mBubbleIconFactory; private Bubble mExpandedBubble; private boolean mIsExpanded; - private boolean mImeVisible; /** Whether the stack is currently on the left side of the screen, or animating there. */ private boolean mStackOnLeftOrWillBe = false; @@ -191,9 +199,20 @@ public class BubbleStackView extends FrameLayout { /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */ private boolean mIsGestureInProgress = false; + /** Description of current animation controller state. */ + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("Stack view state:"); + pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress); + pw.print(" showingDismiss: "); pw.println(mShowingDismiss); + pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating); + pw.print(" draggingInDismiss: "); pw.println(mDraggingInDismissTarget); + pw.print(" animatingMagnet: "); pw.println(mAnimatingMagnet); + mStackAnimationController.dump(fd, pw, args); + mExpandedAnimationController.dump(fd, pw, args); + } + private BubbleTouchHandler mTouchHandler; private BubbleController.BubbleExpandListener mExpandListener; - private BubbleExpandedView.OnBubbleBlockedListener mBlockedListener; private boolean mViewUpdatedRequested = false; private boolean mIsExpansionAnimating = false; @@ -225,7 +244,7 @@ public class BubbleStackView extends FrameLayout { @Override public boolean onPreDraw() { getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); - applyCurrentState(); + updateExpandedView(); mViewUpdatedRequested = false; return true; } @@ -270,6 +289,11 @@ public class BubbleStackView extends FrameLayout { private float mFlyoutDragDeltaX = 0f; /** + * Runnable that animates in the flyout. This reference is needed to cancel delayed postings. + */ + private Runnable mAnimateInFlyout; + + /** * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides * it immediately. */ @@ -282,13 +306,13 @@ public class BubbleStackView extends FrameLayout { } }; - @NonNull private final SurfaceSynchronizer mSurfaceSynchronizer; + @NonNull + private final SurfaceSynchronizer mSurfaceSynchronizer; private BubbleDismissView mDismissContainer; private Runnable mAfterMagnet; - private boolean mSuppressNewDot = false; - private boolean mSuppressFlyout = false; + private int mOrientation = Configuration.ORIENTATION_UNDEFINED; public BubbleStackView(Context context, BubbleData data, @Nullable SurfaceSynchronizer synchronizer) { @@ -302,7 +326,8 @@ public class BubbleStackView extends FrameLayout { Resources res = getResources(); mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); - mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); + mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding); mExpandedAnimateXDistance = res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance); mExpandedAnimateYDistance = @@ -311,8 +336,6 @@ public class BubbleStackView extends FrameLayout { mStatusBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); - mPipDismissHeight = mContext.getResources().getDimensionPixelSize( - R.dimen.pip_dismiss_gradient_height); mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); mDisplaySize = new Point(); @@ -325,8 +348,9 @@ public class BubbleStackView extends FrameLayout { int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); mStackAnimationController = new StackAnimationController(); + mExpandedAnimationController = new ExpandedAnimationController( - mDisplaySize, mExpandedViewPadding); + mDisplaySize, mExpandedViewPadding, res.getConfiguration().orientation); mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER; mBubbleContainer = new PhysicsAnimationLayout(context); @@ -335,6 +359,8 @@ public class BubbleStackView extends FrameLayout { mBubbleContainer.setClipChildren(false); addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + mBubbleIconFactory = new BubbleIconFactory(context); + mExpandedViewContainer = new FrameLayout(context); mExpandedViewContainer.setElevation(elevation); mExpandedViewContainer.setPadding(mExpandedViewPadding, mExpandedViewPadding, @@ -342,15 +368,9 @@ public class BubbleStackView extends FrameLayout { mExpandedViewContainer.setClipChildren(false); addView(mExpandedViewContainer); - mFlyout = new BubbleFlyoutView(context); - mFlyout.setVisibility(GONE); - mFlyout.animate() - .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION) - .setInterpolator(new AccelerateDecelerateInterpolator()); - addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); - + setUpFlyout(); mFlyoutTransitionSpring.setSpring(new SpringForce() - .setStiffness(SpringForce.STIFFNESS_MEDIUM) + .setStiffness(SpringForce.STIFFNESS_LOW) .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring); @@ -361,13 +381,6 @@ public class BubbleStackView extends FrameLayout { Gravity.BOTTOM)); addView(mDismissContainer); - mDismissContainer = new BubbleDismissView(mContext); - mDismissContainer.setLayoutParams(new FrameLayout.LayoutParams( - MATCH_PARENT, - getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height), - Gravity.BOTTOM)); - addView(mDismissContainer); - mExpandedViewXAnim = new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X); mExpandedViewXAnim.setSpring( @@ -383,7 +396,7 @@ public class BubbleStackView extends FrameLayout { .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); mExpandedViewYAnim.addEndListener((anim, cancelled, value, velocity) -> { if (mIsExpanded && mExpandedBubble != null) { - mExpandedBubble.expandedView.updateView(); + mExpandedBubble.getExpandedView().updateView(); } }); @@ -392,34 +405,30 @@ public class BubbleStackView extends FrameLayout { mBubbleContainer.bringToFront(); setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> { - final int keyboardHeight = insets.getSystemWindowInsetBottom() - - insets.getStableInsetBottom(); if (!mIsExpanded || mIsExpansionAnimating) { return view.onApplyWindowInsets(insets); } - mImeVisible = keyboardHeight != 0; - - float newY = getYPositionForExpandedView(); - if (newY < 0) { - // TODO: This means our expanded content is too big to fit on screen. Right now - // we'll let it translate off but we should be clipping it & pushing the header - // down so that it always remains visible. - } - mExpandedViewYAnim.animateToFinalPosition(newY); mExpandedAnimationController.updateYPosition( // Update the insets after we're done translating otherwise position // calculation for them won't be correct. - () -> mExpandedBubble.expandedView.updateInsets(insets)); + () -> mExpandedBubble.getExpandedView().updateInsets(insets)); return view.onApplyWindowInsets(insets); }); - mMoveStackToValidPositionOnLayoutListener = + mOrientationChangedListener = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + mExpandedAnimationController.updateOrientation(mOrientation); + if (mIsExpanded) { + // Re-draw bubble row and pointer for new orientation. + mExpandedAnimationController.expandFromStack(() -> { + updatePointerPosition(); + } /* after */); + } if (mVerticalPosPercentBeforeRotation >= 0) { mStackAnimationController.moveStackToSimilarPositionAfterRotation( mWasOnLeftBeforeRotation, mVerticalPosPercentBeforeRotation); } - removeOnLayoutChangeListener(mMoveStackToValidPositionOnLayoutListener); + removeOnLayoutChangeListener(mOrientationChangedListener); }; // This must be a separate OnDrawListener since it should be called for every draw. @@ -449,25 +458,48 @@ public class BubbleStackView extends FrameLayout { }); } + private void setUpFlyout() { + if (mFlyout != null) { + removeView(mFlyout); + } + mFlyout = new BubbleFlyoutView(getContext()); + mFlyout.setVisibility(GONE); + mFlyout.animate() + .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION) + .setInterpolator(new AccelerateDecelerateInterpolator()); + addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + } + /** * Handle theme changes. */ public void onThemeChanged() { + // Recreate icon factory to update default adaptive icon scale. + mBubbleIconFactory = new BubbleIconFactory(mContext); + setUpFlyout(); for (Bubble b: mBubbleData.getBubbles()) { - b.iconView.updateViews(); - b.expandedView.applyThemeAttrs(); + b.getIconView().setBubbleIconFactory(mBubbleIconFactory); + b.getIconView().updateViews(); + b.getExpandedView().applyThemeAttrs(); } } /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */ - public void onOrientationChanged() { + public void onOrientationChanged(int orientation) { + mOrientation = orientation; + + // Some resources change depending on orientation + Resources res = getContext().getResources(); + mStatusBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); + final RectF allowablePos = mStackAnimationController.getAllowableStackPositionRegion(); mWasOnLeftBeforeRotation = mStackAnimationController.isStackOnLeftSide(); mVerticalPosPercentBeforeRotation = (mStackAnimationController.getStackPosition().y - allowablePos.top) / (allowablePos.bottom - allowablePos.top); - addOnLayoutChangeListener(mMoveStackToValidPositionOnLayoutListener); - + addOnLayoutChangeListener(mOrientationChangedListener); hideFlyoutImmediate(); } @@ -483,18 +515,6 @@ public class BubbleStackView extends FrameLayout { } @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - float x = ev.getRawX(); - float y = ev.getRawY(); - // If we're expanded only intercept if the tap is outside of the widget container - if (mIsExpanded && isIntersecting(mExpandedViewContainer, x, y)) { - return false; - } else { - return isIntersecting(mBubbleContainer, x, y); - } - } - - @Override public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfoInternal(info); @@ -570,7 +590,7 @@ public class BubbleStackView extends FrameLayout { } Bubble topBubble = mBubbleData.getBubbles().get(0); String appName = topBubble.getAppName(); - Notification notification = topBubble.entry.notification.getNotification(); + Notification notification = topBubble.getEntry().notification.getNotification(); CharSequence titleCharSeq = notification.extras.getCharSequence(Notification.EXTRA_TITLE); String titleStr = getResources().getString(R.string.stream_notification); if (titleCharSeq != null) { @@ -616,6 +636,7 @@ public class BubbleStackView extends FrameLayout { /** * Updates the visibility of the 'dot' indicating an update on the bubble. + * * @param key the {@link NotificationEntry#key} associated with the bubble. */ public void updateDotVisibility(String key) { @@ -640,10 +661,17 @@ public class BubbleStackView extends FrameLayout { } /** + * Whether the stack of bubbles is animating to or from expansion. + */ + public boolean isExpansionAnimating() { + return mIsExpansionAnimating; + } + + /** * The {@link BubbleView} that is expanded, null if one does not exist. */ BubbleView getExpandedBubbleView() { - return mExpandedBubble != null ? mExpandedBubble.iconView : null; + return mExpandedBubble != null ? mExpandedBubble.getIconView() : null; } /** @@ -664,36 +692,33 @@ public class BubbleStackView extends FrameLayout { Bubble bubbleToExpand = mBubbleData.getBubbleWithKey(key); if (bubbleToExpand != null) { setSelectedBubble(bubbleToExpand); - bubbleToExpand.entry.setShowInShadeWhenBubble(false); + bubbleToExpand.setShowInShadeWhenBubble(false); setExpanded(true); } } - /** - * Sets the entry that should be expanded and expands if needed. - */ - @VisibleForTesting - void setExpandedBubble(NotificationEntry entry) { - for (int i = 0; i < mBubbleContainer.getChildCount(); i++) { - BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i); - if (entry.equals(bv.getEntry())) { - setExpandedBubble(entry.key); - } - } - } - // via BubbleData.Listener void addBubble(Bubble bubble) { - if (DEBUG) { + if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "addBubble: " + bubble); } + + if (mBubbleContainer.getChildCount() == 0) { + mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); + } + bubble.inflate(mInflater, this); - mBubbleContainer.addView(bubble.iconView, 0, + bubble.getIconView().setBubbleIconFactory(mBubbleIconFactory); + bubble.getIconView().updateViews(); + + // Set the dot position to the opposite of the side the stack is resting on, since the stack + // resting slightly off-screen would result in the dot also being off-screen. + bubble.getIconView().setDotPosition( + !mStackOnLeftOrWillBe /* onLeft */, false /* animate */); + + mBubbleContainer.addView(bubble.getIconView(), 0, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); - ViewClippingUtil.setClippingDeactivated(bubble.iconView, true, mClippingParameters); - if (bubble.iconView != null) { - bubble.iconView.setSuppressDot(mSuppressNewDot, false /* animate */); - } + ViewClippingUtil.setClippingDeactivated(bubble.getIconView(), true, mClippingParameters); animateInFlyoutForBubble(bubble); requestUpdate(); logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__POSTED); @@ -702,13 +727,14 @@ public class BubbleStackView extends FrameLayout { // via BubbleData.Listener void removeBubble(Bubble bubble) { - if (DEBUG) { + if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "removeBubble: " + bubble); } // Remove it from the views - int removedIndex = mBubbleContainer.indexOfChild(bubble.iconView); + int removedIndex = mBubbleContainer.indexOfChild(bubble.getIconView()); if (removedIndex >= 0) { mBubbleContainer.removeViewAt(removedIndex); + bubble.cleanupExpandedState(); logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); } else { Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble); @@ -726,8 +752,10 @@ public class BubbleStackView extends FrameLayout { public void updateBubbleOrder(List<Bubble> bubbles) { for (int i = 0; i < bubbles.size(); i++) { Bubble bubble = bubbles.get(i); - mBubbleContainer.reorderView(bubble.iconView, i); + mBubbleContainer.reorderView(bubble.getIconView(), i); } + + updateBubbleZOrdersAndDotPosition(false /* animate */); } /** @@ -737,7 +765,7 @@ public class BubbleStackView extends FrameLayout { */ // via BubbleData.Listener public void setSelectedBubble(@Nullable Bubble bubbleToSelect) { - if (DEBUG) { + if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "setSelectedBubble: " + bubbleToSelect); } if (mExpandedBubble != null && mExpandedBubble.equals(bubbleToSelect)) { @@ -760,9 +788,8 @@ public class BubbleStackView extends FrameLayout { requestUpdate(); logBubbleEvent(previouslySelected, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); logBubbleEvent(bubbleToSelect, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); - notifyExpansionChanged(previouslySelected.entry, false /* expanded */); - notifyExpansionChanged(bubbleToSelect == null ? null : bubbleToSelect.entry, - true /* expanded */); + notifyExpansionChanged(previouslySelected, false /* expanded */); + notifyExpansionChanged(bubbleToSelect, true /* expanded */); }); } } @@ -774,42 +801,32 @@ public class BubbleStackView extends FrameLayout { */ // via BubbleData.Listener public void setExpanded(boolean shouldExpand) { - if (DEBUG) { + if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "setExpanded: " + shouldExpand); } - boolean wasExpanded = mIsExpanded; - if (shouldExpand == wasExpanded) { + if (shouldExpand == mIsExpanded) { return; } - if (wasExpanded) { - // Collapse the stack - mExpandedViewContainer.setAlpha(0.0f); - // TODO: In order to prevent flicker, code below should be executed after the alpha - // value set on the mExpandedViewContainer is reflected on the screen. However, we - // cannot just postpone the execution like #setSelectedBubble(), since some of member - // variables referred by the code are overridden before the execution. - if (mExpandedBubble != null) { - mExpandedBubble.setContentVisibility(false); - } - animateExpansion(false /* expand */); + if (mIsExpanded) { + animateCollapse(); logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); } else { - // Expand the stack - animateExpansion(true /* expand */); + animateExpansion(); // TODO: move next line to BubbleData logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED); } - notifyExpansionChanged(mExpandedBubble.entry, mIsExpanded); + notifyExpansionChanged(mExpandedBubble, mIsExpanded); } /** * Dismiss the stack of bubbles. + * * @deprecated */ @Deprecated void stackDismissed(int reason) { - if (DEBUG) { + if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "stackDismissed: reason=" + reason); } mBubbleData.dismissAll(reason); @@ -826,21 +843,23 @@ public class BubbleStackView extends FrameLayout { float y = event.getRawY(); if (mIsExpanded) { if (isIntersecting(mBubbleContainer, x, y)) { + // Could be tapping or dragging a bubble while expanded for (int i = 0; i < mBubbleContainer.getChildCount(); i++) { BubbleView view = (BubbleView) mBubbleContainer.getChildAt(i); if (isIntersecting(view, x, y)) { return view; } } - } else if (isIntersecting(mExpandedViewContainer, x, y)) { - return mExpandedViewContainer; } - // Outside parts of view we care about. + BubbleExpandedView bev = (BubbleExpandedView) mExpandedViewContainer.getChildAt(0); + if (bev.intersectingTouchableContent((int) x, (int) y)) { + return bev; + } + // Outside of the parts we care about. return null; } else if (mFlyout.getVisibility() == VISIBLE && isIntersecting(mFlyout, x, y)) { return mFlyout; } - // If it wasn't an individual bubble in the expanded state, or the flyout, it's the stack. return this; } @@ -859,7 +878,7 @@ public class BubbleStackView extends FrameLayout { @Deprecated @MainThread void collapseStack() { - if (DEBUG) { + if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "collapseStack()"); } mBubbleData.setExpanded(false); @@ -871,7 +890,7 @@ public class BubbleStackView extends FrameLayout { @Deprecated @MainThread void collapseStack(Runnable endRunnable) { - if (DEBUG) { + if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "collapseStack(endRunnable)"); } collapseStack(); @@ -889,73 +908,83 @@ public class BubbleStackView extends FrameLayout { @Deprecated @MainThread void expandStack() { - if (DEBUG) { + if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "expandStack()"); } mBubbleData.setExpanded(true); } - /** - * Tell the stack to animate to collapsed or expanded state. - */ - private void animateExpansion(boolean shouldExpand) { - if (DEBUG) { - Log.d(TAG, "animateExpansion: shouldExpand=" + shouldExpand); - } - if (mIsExpanded != shouldExpand) { - hideFlyoutImmediate(); + private void beforeExpandedViewAnimation() { + hideFlyoutImmediate(); + updateExpandedBubble(); + updateExpandedView(); + mIsExpansionAnimating = true; + } - mIsExpanded = shouldExpand; - updateExpandedBubble(); - applyCurrentState(); + private void afterExpandedViewAnimation() { + updateExpandedView(); + mIsExpansionAnimating = false; + requestUpdate(); + } - mIsExpansionAnimating = true; + private void animateCollapse() { + mIsExpanded = false; + final Bubble previouslySelected = mExpandedBubble; + beforeExpandedViewAnimation(); + + mBubbleContainer.cancelAllAnimations(); + mExpandedAnimationController.collapseBackToStack( + mStackAnimationController.getStackPositionAlongNearestHorizontalEdge() + /* collapseTo */, + () -> { + mBubbleContainer.setActiveController(mStackAnimationController); + afterExpandedViewAnimation(); + previouslySelected.setContentVisibility(false); + }); - Runnable updateAfter = () -> { - applyCurrentState(); - mIsExpansionAnimating = false; - requestUpdate(); - }; + mExpandedViewXAnim.animateToFinalPosition(getCollapsedX()); + mExpandedViewYAnim.animateToFinalPosition(getCollapsedY()); + mExpandedViewContainer.animate() + .setDuration(100) + .alpha(0f); + } - if (shouldExpand) { - mBubbleContainer.setActiveController(mExpandedAnimationController); - mExpandedAnimationController.expandFromStack(() -> { - updatePointerPosition(); - updateAfter.run(); - } /* after */); - } else { - mBubbleContainer.cancelAllAnimations(); - mExpandedAnimationController.collapseBackToStack( - mStackAnimationController.getStackPositionAlongNearestHorizontalEdge(), - () -> { - mBubbleContainer.setActiveController(mStackAnimationController); - updateAfter.run(); - }); - } + private void animateExpansion() { + mIsExpanded = true; + beforeExpandedViewAnimation(); - final float xStart = - mStackAnimationController.getStackPosition().x < getWidth() / 2 - ? -mExpandedAnimateXDistance - : mExpandedAnimateXDistance; + mBubbleContainer.setActiveController(mExpandedAnimationController); + mExpandedAnimationController.expandFromStack(() -> { + updatePointerPosition(); + afterExpandedViewAnimation(); + } /* after */); - final float yStart = Math.min( - mStackAnimationController.getStackPosition().y, - mExpandedAnimateYDistance); - final float yDest = getYPositionForExpandedView(); - if (shouldExpand) { - mExpandedViewContainer.setTranslationX(xStart); - mExpandedViewContainer.setTranslationY(yStart); - } + mExpandedViewContainer.setTranslationX(getCollapsedX()); + mExpandedViewContainer.setTranslationY(getCollapsedY()); + mExpandedViewContainer.setAlpha(0f); - mExpandedViewXAnim.animateToFinalPosition(shouldExpand ? 0f : xStart); - mExpandedViewYAnim.animateToFinalPosition(shouldExpand ? yDest : yStart); - } + mExpandedViewXAnim.animateToFinalPosition(0f); + mExpandedViewYAnim.animateToFinalPosition(getExpandedViewY()); + mExpandedViewContainer.animate() + .setDuration(100) + .alpha(1f); + } + + private float getCollapsedX() { + return mStackAnimationController.getStackPosition().x < getWidth() / 2 + ? -mExpandedAnimateXDistance + : mExpandedAnimateXDistance; + } + + private float getCollapsedY() { + return Math.min(mStackAnimationController.getStackPosition().y, + mExpandedAnimateYDistance); } - private void notifyExpansionChanged(NotificationEntry entry, boolean expanded) { - if (mExpandListener != null) { - mExpandListener.onBubbleExpandChanged(expanded, entry != null ? entry.key : null); + private void notifyExpansionChanged(Bubble bubble, boolean expanded) { + if (mExpandListener != null && bubble != null) { + mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey()); } } @@ -968,7 +997,7 @@ public class BubbleStackView extends FrameLayout { /** Moves the bubbles out of the way if they're going to be over the keyboard. */ public void onImeVisibilityChanged(boolean visible, int height) { - mStackAnimationController.setImeHeight(height + mImeOffset); + mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0); if (!mIsExpanded) { mStackAnimationController.animateForImeVisibility(visible); @@ -977,7 +1006,7 @@ public class BubbleStackView extends FrameLayout { /** Called when a drag operation on an individual bubble has started. */ public void onBubbleDragStart(View bubble) { - if (DEBUG) { + if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "onBubbleDragStart: bubble=" + bubble); } mExpandedAnimationController.prepareForBubbleDrag(bubble); @@ -996,7 +1025,7 @@ public class BubbleStackView extends FrameLayout { /** Called when a drag operation on an individual bubble has finished. */ public void onBubbleDragFinish( View bubble, float x, float y, float velX, float velY) { - if (DEBUG) { + if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "onBubbleDragFinish: bubble=" + bubble); } @@ -1005,11 +1034,11 @@ public class BubbleStackView extends FrameLayout { } mExpandedAnimationController.snapBubbleBack(bubble, velX, velY); - springOutDismissTargetAndHideCircle(); + hideDismissTarget(); } void onDragStart() { - if (DEBUG) { + if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "onDragStart()"); } if (mIsExpanded || mIsExpansionAnimating) { @@ -1033,7 +1062,7 @@ public class BubbleStackView extends FrameLayout { } void onDragFinish(float x, float y, float velX, float velY) { - if (DEBUG) { + if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "onDragFinish"); } @@ -1046,8 +1075,8 @@ public class BubbleStackView extends FrameLayout { StatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED); mStackOnLeftOrWillBe = newStackX <= 0; - updateBubbleShadowsAndDotPosition(true /* animate */); - springOutDismissTargetAndHideCircle(); + updateBubbleZOrdersAndDotPosition(true /* animate */); + hideDismissTarget(); } void onFlyoutDragStart() { @@ -1055,6 +1084,12 @@ public class BubbleStackView extends FrameLayout { } void onFlyoutDragged(float deltaX) { + // This shouldn't happen, but if it does, just wait until the flyout lays out. This method + // is continually called. + if (mFlyout.getWidth() <= 0) { + return; + } + final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); mFlyoutDragDeltaX = deltaX; @@ -1062,7 +1097,7 @@ public class BubbleStackView extends FrameLayout { onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth(); mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent))); - // Calculate how to translate the flyout if it has been dragged too far in etiher direction. + // Calculate how to translate the flyout if it has been dragged too far in either direction. float overscrollTranslation = 0f; if (collapsePercent < 0f || collapsePercent > 1f) { // Whether we are more than 100% transitioned to the dot. @@ -1073,7 +1108,6 @@ public class BubbleStackView extends FrameLayout { // after it has already become the dot. final boolean overscrollingLeft = (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f); - overscrollTranslation = (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1) * (overscrollingLeft ? -1 : 1) @@ -1086,6 +1120,19 @@ public class BubbleStackView extends FrameLayout { } /** + * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout + * once it collapses. + */ + @Nullable private Bubble mBubbleToExpandAfterFlyoutCollapse = null; + + void onFlyoutTapped() { + mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble(); + + mFlyout.removeCallbacks(mHideFlyout); + mHideFlyout.run(); + } + + /** * Called when the flyout drag has finished, and returns true if the gesture successfully * dismissed the flyout. */ @@ -1181,9 +1228,6 @@ public class BubbleStackView extends FrameLayout { animateDesaturateAndDarken(magnetView, true); } - - mDismissContainer.animateEncircleCenterWithX(true); - } else { mAnimatingMagnet = false; @@ -1194,8 +1238,6 @@ public class BubbleStackView extends FrameLayout { mExpandedAnimationController.demagnetizeBubbleTo(x, y, velX, velY); animateDesaturateAndDarken(magnetView, false); } - - mDismissContainer.animateEncircleCenterWithX(false); } mVibrator.vibrate(VibrationEffect.get(toTarget @@ -1214,7 +1256,7 @@ public class BubbleStackView extends FrameLayout { mAfterMagnet = null; mVibrator.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_CLICK)); - mDismissContainer.animateEncirclingCircleDisappearance(); + mDismissContainer.springOut(); // 'Implode' the stack and then hide the dismiss target. if (touchedView == this) { @@ -1252,7 +1294,7 @@ public class BubbleStackView extends FrameLayout { } } - /** Animates in the dismiss target, including the gradient behind it. */ + /** Animates in the dismiss target. */ private void springInDismissTarget() { if (mShowingDismiss) { return; @@ -1270,7 +1312,7 @@ public class BubbleStackView extends FrameLayout { * Animates the dismiss target out, as well as the circle that encircles the bubbles, if they * were dragged into the target and encircled. */ - private void springOutDismissTargetAndHideCircle() { + private void hideDismissTarget() { if (!mShowingDismiss) { return; } @@ -1287,6 +1329,12 @@ public class BubbleStackView extends FrameLayout { /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */ private void animateFlyoutCollapsed(boolean collapsed, float velX) { final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); + // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's + // faster. + mFlyoutTransitionSpring.getSpring().setStiffness( + (mBubbleToExpandAfterFlyoutCollapse != null) + ? SpringForce.STIFFNESS_MEDIUM + : SpringForce.STIFFNESS_LOW); mFlyoutTransitionSpring .setStartValue(mFlyoutDragDeltaX) .setStartVelocity(velX) @@ -1295,123 +1343,121 @@ public class BubbleStackView extends FrameLayout { : 0f); } - /** - * Calculates how large the expanded view of the bubble can be. This takes into account the - * y position when the bubbles are expanded as well as the bounds of the dismiss target. - */ - int getMaxExpandedHeight() { - int expandedY = (int) mExpandedAnimationController.getExpandedY(); - // PIP dismiss view uses FLAG_LAYOUT_IN_SCREEN so we need to subtract the bottom inset - int pipDismissHeight = mPipDismissHeight - getBottomInset(); - return mDisplaySize.y - expandedY - mBubbleSize - pipDismissHeight; + /** Updates the dot visibility, this is used in response to a zen mode config change. */ + void updateDots() { + int bubbsCount = mBubbleContainer.getChildCount(); + for (int i = 0; i < bubbsCount; i++) { + BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i); + // If nothing changed the animation won't happen + bv.updateDotVisibility(true /* animate */); + } } /** * Calculates the y position of the expanded view when it is expanded. */ - float getYPositionForExpandedView() { - return getStatusBarHeight() + mBubbleSize + mBubblePadding + mPointerHeight; + float getExpandedViewY() { + return getStatusBarHeight() + mBubbleSize + mBubblePaddingTop + mPointerHeight; } /** - * Called when the height of the currently expanded view has changed (not via an - * update to the bubble's desired height but for some other reason, e.g. permission view - * goes away). + * Animates in the flyout for the given bubble, if available, and then hides it after some time. */ - void onExpandedHeightChanged() { - if (mIsExpanded) { - requestUpdate(); - } - } + @VisibleForTesting + void animateInFlyoutForBubble(Bubble bubble) { + final CharSequence updateMessage = bubble.getUpdateMessage(getContext()); - /** Sets whether all bubbles in the stack should not show the 'new' dot. */ - void setSuppressNewDot(boolean suppressNewDot) { - mSuppressNewDot = suppressNewDot; + if (!bubble.showFlyoutForBubble()) { + // In case flyout was suppressed for this update, reset now. + bubble.setSuppressFlyout(false); + return; + } - for (int i = 0; i < mBubbleContainer.getChildCount(); i++) { - BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i); - bv.setSuppressDot(suppressNewDot, true /* animate */); + if (updateMessage == null + || isExpanded() + || mIsExpansionAnimating + || mIsGestureInProgress + || mBubbleToExpandAfterFlyoutCollapse != null) { + // Skip the message if none exists, we're expanded or animating expansion, or we're + // about to expand a bubble from the previous tapped flyout. + return; } - } - /** - * Sets whether the flyout should not appear, even if the notif otherwise would generate one. - */ - void setSuppressFlyout(boolean suppressFlyout) { - mSuppressFlyout = suppressFlyout; - } + if (bubble.getIconView() != null) { + // Temporarily suppress the dot while the flyout is visible. + bubble.getIconView().setSuppressDot( + true /* suppressDot */, false /* animate */); - /** - * Callback to run after the flyout hides. Also called if a new flyout is shown before the - * previous one animates out. - */ - private Runnable mAfterFlyoutHides; + mFlyout.removeCallbacks(mAnimateInFlyout); + mFlyoutDragDeltaX = 0f; - /** - * Animates in the flyout for the given bubble, if available, and then hides it after some time. - */ - @VisibleForTesting - void animateInFlyoutForBubble(Bubble bubble) { - final CharSequence updateMessage = bubble.entry.getUpdateMessage(getContext()); - - // Show the message if one exists, and we're not expanded or animating expansion. - if (updateMessage != null - && !isExpanded() - && !mIsExpansionAnimating - && !mIsGestureInProgress - && !mSuppressFlyout) { - if (bubble.iconView != null) { - // Temporarily suppress the dot while the flyout is visible. - bubble.iconView.setSuppressDot( - true /* suppressDot */, false /* animate */); - - mFlyoutDragDeltaX = 0f; - mFlyout.setAlpha(0f); - - if (mAfterFlyoutHides != null) { - mAfterFlyoutHides.run(); + if (mAfterFlyoutHides != null) { + mAfterFlyoutHides.run(); + } + + mAfterFlyoutHides = () -> { + final boolean suppressDot = !bubble.showBubbleDot(); + // If we're going to suppress the dot, make it visible first so it'll + // visibly animate away. + if (suppressDot) { + bubble.getIconView().setSuppressDot( + false /* suppressDot */, false /* animate */); } + // Reset dot suppression. If we're not suppressing due to DND, then + // stop suppressing it with no animation (since the flyout has + // transformed into the dot). If we are suppressing due to DND, animate + // it away. + bubble.getIconView().setSuppressDot( + suppressDot /* suppressDot */, + suppressDot /* animate */); + + if (mBubbleToExpandAfterFlyoutCollapse != null) { + mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse); + mBubbleData.setExpanded(true); + mBubbleToExpandAfterFlyoutCollapse = null; + } + }; - mAfterFlyoutHides = () -> { - if (bubble.iconView == null) { - return; - } + mFlyout.setVisibility(INVISIBLE); - // If we're going to suppress the dot, make it visible first so it'll - // visibly animate away. - if (mSuppressNewDot) { - bubble.iconView.setSuppressDot( - false /* suppressDot */, false /* animate */); - } + // Post in case layout isn't complete and getWidth returns 0. + post(() -> { + // An auto-expanding bubble could have been posted during the time it takes to + // layout. + if (isExpanded()) { + return; + } - // Reset dot suppression. If we're not suppressing due to DND, then - // stop suppressing it with no animation (since the flyout has - // transformed into the dot). If we are suppressing due to DND, animate - // it away. - bubble.iconView.setSuppressDot( - mSuppressNewDot /* suppressDot */, - mSuppressNewDot /* animate */); + final Runnable afterShow = () -> { + mAnimateInFlyout = () -> { + mFlyout.setVisibility(VISIBLE); + bubble.getIconView().setSuppressDot( + true /* suppressDot */, false /* animate */); + mFlyoutDragDeltaX = + mStackAnimationController.isStackOnLeftSide() + ? -mFlyout.getWidth() + : mFlyout.getWidth(); + animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */); + mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); + }; + + mFlyout.postDelayed(mAnimateInFlyout, 200); }; - // Post in case layout isn't complete and getWidth returns 0. - post(() -> { - // An auto-expanding bubble could have been posted during the time it takes to - // layout. - if (isExpanded()) { - return; - } - - mFlyout.showFlyout( - updateMessage, mStackAnimationController.getStackPosition(), getWidth(), - mStackAnimationController.isStackOnLeftSide(), - bubble.iconView.getBadgeColor(), mAfterFlyoutHides); - }); - } - - mFlyout.removeCallbacks(mHideFlyout); - mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); - logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT); + mFlyout.setupFlyoutStartingAsDot( + updateMessage, mStackAnimationController.getStackPosition(), getWidth(), + mStackAnimationController.isStackOnLeftSide(), + bubble.getIconView().getBadgeColor(), + afterShow, + mAfterFlyoutHides, + bubble.getIconView().getDotCenter()); + mFlyout.bringToFront(); + }); } + + mFlyout.removeCallbacks(mHideFlyout); + mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); + logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT); } /** Hide the flyout immediately and cancel any pending hide runnables. */ @@ -1420,6 +1466,7 @@ public class BubbleStackView extends FrameLayout { mAfterFlyoutHides.run(); } + mFlyout.removeCallbacks(mAnimateInFlyout); mFlyout.removeCallbacks(mHideFlyout); mFlyout.hideFlyout(); } @@ -1430,6 +1477,11 @@ public class BubbleStackView extends FrameLayout { if (mBubbleContainer.getChildCount() > 0) { mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect); } + // Increase the touch target size of the bubble + outRect.top -= mBubbleTouchPadding; + outRect.left -= mBubbleTouchPadding; + outRect.right += mBubbleTouchPadding; + outRect.bottom += mBubbleTouchPadding; } else { mBubbleContainer.getBoundsOnScreen(outRect); } @@ -1454,14 +1506,6 @@ public class BubbleStackView extends FrameLayout { return 0; } - private int getBottomInset() { - if (getRootWindowInsets() != null) { - WindowInsets insets = getRootWindowInsets(); - return insets.getSystemWindowInsetBottom(); - } - return 0; - } - private boolean isIntersecting(View view, float x, float y) { mTempLoc = view.getLocationOnScreen(); mTempRect.set(mTempLoc[0], mTempLoc[1], mTempLoc[0] + view.getWidth(), @@ -1479,33 +1523,33 @@ public class BubbleStackView extends FrameLayout { } private void updateExpandedBubble() { - if (DEBUG) { + if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "updateExpandedBubble()"); } mExpandedViewContainer.removeAllViews(); if (mExpandedBubble != null && mIsExpanded) { - mExpandedViewContainer.addView(mExpandedBubble.expandedView); - mExpandedBubble.expandedView.populateExpandedView(); + mExpandedViewContainer.addView(mExpandedBubble.getExpandedView()); + mExpandedBubble.getExpandedView().populateExpandedView(); mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); mExpandedViewContainer.setAlpha(1.0f); } } - private void applyCurrentState() { - if (DEBUG) { - Log.d(TAG, "applyCurrentState: mIsExpanded=" + mIsExpanded); + private void updateExpandedView() { + if (DEBUG_BUBBLE_STACK_VIEW) { + Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded); } mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); if (mIsExpanded) { // First update the view so that it calculates a new height (ensuring the y position // calculation is correct) - mExpandedBubble.expandedView.updateView(); - final float y = getYPositionForExpandedView(); + mExpandedBubble.getExpandedView().updateView(); + final float y = getExpandedViewY(); if (!mExpandedViewYAnim.isRunning()) { // We're not animating so set the value mExpandedViewContainer.setTranslationY(y); - mExpandedBubble.expandedView.updateView(); + mExpandedBubble.getExpandedView().updateView(); } else { // We are animating so update the value; there is an end listener on the animator // that will ensure expandedeView.updateView gets called. @@ -1514,29 +1558,17 @@ public class BubbleStackView extends FrameLayout { } mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); - updateBubbleShadowsAndDotPosition(false); + updateBubbleZOrdersAndDotPosition(false); } /** Sets the appropriate Z-order and dot position for each bubble in the stack. */ - private void updateBubbleShadowsAndDotPosition(boolean animate) { - int bubbsCount = mBubbleContainer.getChildCount(); - for (int i = 0; i < bubbsCount; i++) { + private void updateBubbleZOrdersAndDotPosition(boolean animate) { + int bubbleCount = mBubbleContainer.getChildCount(); + for (int i = 0; i < bubbleCount; i++) { BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i); bv.updateDotVisibility(true /* animate */); bv.setZ((BubbleController.MAX_BUBBLES * getResources().getDimensionPixelSize(R.dimen.bubble_elevation)) - i); - - // Draw the shadow around the circle inscribed within the bubble's bounds. This - // (intentionally) does not draw a shadow behind the update dot, which should be drawing - // its own shadow since it's on a different (higher) plane. - bv.setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setOval(0, 0, mBubbleSize, mBubbleSize); - } - }); - bv.setClipToOutline(false); - // If the dot is on the left, and so is the stack, we need to change the dot position. if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) { bv.setDotPosition(!mStackOnLeftOrWillBe, animate); @@ -1545,7 +1577,7 @@ public class BubbleStackView extends FrameLayout { } private void updatePointerPosition() { - if (DEBUG) { + if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "updatePointerPosition()"); } @@ -1563,7 +1595,7 @@ public class BubbleStackView extends FrameLayout { // Remove padding when deriving pointer location from bubbles. float bubbleCenter = bubbleLeftFromScreenLeft + halfBubble - mExpandedViewPadding; - expandedBubble.expandedView.setPointerPosition(bubbleCenter); + expandedBubble.getExpandedView().setPointerPosition(bubbleCenter); } /** @@ -1584,7 +1616,7 @@ public class BubbleStackView extends FrameLayout { if (bubble == null) { return 0; } - return mBubbleContainer.indexOfChild(bubble.iconView); + return mBubbleContainer.indexOfChild(bubble.getIconView()); } /** @@ -1617,8 +1649,8 @@ public class BubbleStackView extends FrameLayout { * @param action the user interaction enum. */ private void logBubbleEvent(@Nullable Bubble bubble, int action) { - if (bubble == null || bubble.entry == null - || bubble.entry.notification == null) { + if (bubble == null || bubble.getEntry() == null + || bubble.getEntry().notification == null) { StatsLog.write(StatsLog.BUBBLE_UI_CHANGED, null /* package name */, null /* notification channel */, @@ -1630,9 +1662,9 @@ public class BubbleStackView extends FrameLayout { getNormalizedYPosition(), false /* unread bubble */, false /* on-going bubble */, - false /* foreground bubble */); + false /* isAppForeground (unused) */); } else { - StatusBarNotification notification = bubble.entry.notification; + StatusBarNotification notification = bubble.getEntry().notification; StatsLog.write(StatsLog.BUBBLE_UI_CHANGED, notification.getPackageName(), notification.getNotification().getChannelId(), @@ -1642,9 +1674,9 @@ public class BubbleStackView extends FrameLayout { action, getNormalizedXPosition(), getNormalizedYPosition(), - bubble.entry.showInShadeWhenBubble(), - bubble.entry.isForegroundService(), - BubbleController.isForegroundApp(mContext, notification.getPackageName())); + bubble.showInShadeWhenBubble(), + bubble.isOngoing(), + false /* isAppForeground (unused) */); } } @@ -1656,7 +1688,7 @@ public class BubbleStackView extends FrameLayout { if (!isExpanded()) { return false; } - return mExpandedBubble.expandedView.performBackPressIfNeeded(); + return mExpandedBubble.getExpandedView().performBackPressIfNeeded(); } /** For debugging only */ diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java index 8fe8bd305707..4240e06a8800 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java @@ -18,7 +18,6 @@ package com.android.systemui.bubbles; import android.content.Context; import android.graphics.PointF; -import android.os.Handler; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; @@ -45,7 +44,6 @@ class BubbleTouchHandler implements View.OnTouchListener { */ private static final float INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY = 6000f; - private static final String TAG = "BubbleTouchHandler"; /** * When the stack is flung towards the bottom of the screen, it'll be dismissed if it's flung * towards the center of the screen (where the dismiss target is). This value is the width of @@ -66,11 +64,10 @@ class BubbleTouchHandler implements View.OnTouchListener { private int mTouchSlopSquared; private VelocityTracker mVelocityTracker; - private boolean mInDismissTarget; - private Handler mHandler = new Handler(); - /** View that was initially touched, when we received the first ACTION_DOWN event. */ private View mTouchedView; + /** Whether the current touched view is in the dismiss target. */ + private boolean mInDismissTarget; BubbleTouchHandler(BubbleStackView stackView, BubbleData bubbleData, Context context) { @@ -98,6 +95,15 @@ class BubbleTouchHandler implements View.OnTouchListener { return false; } + if (!(mTouchedView instanceof BubbleView) + && !(mTouchedView instanceof BubbleStackView) + && !(mTouchedView instanceof BubbleFlyoutView)) { + // Not touching anything touchable, but we shouldn't collapse (e.g. touching edge + // of expanded view). + resetForNextGesture(); + return false; + } + final boolean isStack = mStack.equals(mTouchedView); final boolean isFlyout = mStack.getFlyoutView().equals(mTouchedView); final float rawX = event.getRawX(); @@ -193,9 +199,8 @@ class BubbleTouchHandler implements View.OnTouchListener { } }); } else if (isFlyout) { - // TODO(b/129768381): Expand if tapped, dismiss if swiped away. if (!mBubbleData.isExpanded() && !mMovedEnough) { - mBubbleData.setExpanded(true); + mStack.onFlyoutTapped(); } } else if (mMovedEnough) { if (isStack) { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java index 6f1ed28d649e..603c4169c169 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java @@ -19,16 +19,23 @@ package com.android.systemui.bubbles; import android.annotation.Nullable; import android.app.Notification; import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Path; import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.graphics.drawable.InsetDrawable; import android.util.AttributeSet; +import android.util.PathParser; import android.widget.FrameLayout; import com.android.internal.graphics.ColorUtils; +import com.android.launcher3.icons.ShadowGenerator; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -38,23 +45,25 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow * A floating object on the screen that can post message updates. */ public class BubbleView extends FrameLayout { - private static final String TAG = "BubbleView"; private static final int DARK_ICON_ALPHA = 180; private static final double ICON_MIN_CONTRAST = 4.1; - private static final int DEFAULT_BACKGROUND_COLOR = Color.LTGRAY; + private static final int DEFAULT_BACKGROUND_COLOR = Color.LTGRAY; // Same value as Launcher3 badge code private static final float WHITE_SCRIM_ALPHA = 0.54f; private Context mContext; private BadgedImageView mBadgedImageView; private int mBadgeColor; - private int mPadding; private int mIconInset; + private Drawable mUserBadgedAppIcon; + + // mBubbleIconFactory cannot be static because it depends on Context. + private BubbleIconFactory mBubbleIconFactory; private boolean mSuppressDot = false; - private NotificationEntry mEntry; + private Bubble mBubble; public BubbleView(Context context) { this(context, null); @@ -71,8 +80,6 @@ public class BubbleView extends FrameLayout { public BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mContext = context; - // XXX: can this padding just be on the view and we look it up? - mPadding = getResources().getDimensionPixelSize(R.dimen.bubble_view_padding); mIconInset = getResources().getDimensionPixelSize(R.dimen.bubble_icon_inset); } @@ -88,16 +95,15 @@ public class BubbleView extends FrameLayout { } /** - * Populates this view with a notification. + * Populates this view with a bubble. * <p> - * This should only be called when a new notification is being set on the view, updates to the - * current notification should use {@link #update(NotificationEntry)}. + * This should only be called when a new bubble is being set on the view, updates to the + * current bubble should use {@link #update(Bubble)}. * - * @param entry the notification to display as a bubble. + * @param bubble the bubble to display in this view. */ - public void setNotif(NotificationEntry entry) { - mEntry = entry; - updateViews(); + public void setBubble(Bubble bubble) { + mBubble = bubble; } /** @@ -105,7 +111,7 @@ public class BubbleView extends FrameLayout { */ @Nullable public NotificationEntry getEntry() { - return mEntry; + return mBubble != null ? mBubble.getEntry() : null; } /** @@ -113,24 +119,34 @@ public class BubbleView extends FrameLayout { */ @Nullable public String getKey() { - return (mEntry != null) ? mEntry.key : null; + return (mBubble != null) ? mBubble.getKey() : null; } /** - * Updates the UI based on the entry, updates badge and animates messages as needed. + * Updates the UI based on the bubble, updates badge and animates messages as needed. */ - public void update(NotificationEntry entry) { - mEntry = entry; + public void update(Bubble bubble) { + mBubble = bubble; updateViews(); } /** + * @param factory Factory for creating normalized bubble icons. + */ + public void setBubbleIconFactory(BubbleIconFactory factory) { + mBubbleIconFactory = factory; + } + + public void setAppIcon(Drawable appIcon) { + mUserBadgedAppIcon = appIcon; + } + /** * @return the {@link ExpandableNotificationRow} view to display notification content when the * bubble is expanded. */ @Nullable public ExpandableNotificationRow getRowView() { - return (mEntry != null) ? mEntry.getRow() : null; + return (mBubble != null) ? mBubble.getEntry().getRow() : null; } /** Changes the dot's visibility to match the bubble view's state. */ @@ -150,18 +166,23 @@ public class BubbleView extends FrameLayout { /** Sets the position of the 'new' dot, animating it out and back in if requested. */ void setDotPosition(boolean onLeft, boolean animate) { - if (animate && onLeft != mBadgedImageView.getDotPosition() && !mSuppressDot) { + if (animate && onLeft != mBadgedImageView.getDotOnLeft() && !mSuppressDot) { animateDot(false /* showDot */, () -> { - mBadgedImageView.setDotPosition(onLeft); + mBadgedImageView.setDotOnLeft(onLeft); animateDot(true /* showDot */, null); }); } else { - mBadgedImageView.setDotPosition(onLeft); + mBadgedImageView.setDotOnLeft(onLeft); } } + float[] getDotCenter() { + float[] unscaled = mBadgedImageView.getDotCenter(); + return new float[]{unscaled[0], unscaled[1]}; + } + boolean getDotPositionOnLeft() { - return mBadgedImageView.getDotPosition(); + return mBadgedImageView.getDotOnLeft(); } /** @@ -169,7 +190,7 @@ public class BubbleView extends FrameLayout { * after animation if requested. */ private void updateDotVisibility(boolean animate, Runnable after) { - boolean showDot = getEntry().showInShadeWhenBubble() && !mSuppressDot; + boolean showDot = mBubble.showBubbleDot() && !mSuppressDot; if (animate) { animateDot(showDot, after); @@ -186,7 +207,6 @@ public class BubbleView extends FrameLayout { if (showDot) { mBadgedImageView.setShowDot(true); } - mBadgedImageView.clearAnimation(); mBadgedImageView.animate().setDuration(200) .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) @@ -207,37 +227,60 @@ public class BubbleView extends FrameLayout { } void updateViews() { - if (mEntry == null) { + if (mBubble == null || mBubbleIconFactory == null) { return; } - Notification.BubbleMetadata metadata = mEntry.getBubbleMetadata(); - Notification n = mEntry.notification.getNotification(); - Icon ic; - boolean needsTint; - if (metadata != null) { - ic = metadata.getIcon(); - needsTint = ic.getType() != Icon.TYPE_ADAPTIVE_BITMAP; - } else { - needsTint = n.getLargeIcon() == null; - ic = needsTint ? n.getSmallIcon() : n.getLargeIcon(); - } + // Update icon. + Notification.BubbleMetadata metadata = mBubble.getEntry().getBubbleMetadata(); + Notification n = mBubble.getEntry().notification.getNotification(); + Icon ic = metadata.getIcon(); + boolean needsTint = ic.getType() != Icon.TYPE_ADAPTIVE_BITMAP; + Drawable iconDrawable = ic.loadDrawable(mContext); if (needsTint) { - mBadgedImageView.setImageDrawable(buildIconWithTint(iconDrawable, n.color)); - } else { - mBadgedImageView.setImageDrawable(iconDrawable); + iconDrawable = buildIconWithTint(iconDrawable, n.color); } + Bitmap bubbleIcon = mBubbleIconFactory.createBadgedIconBitmap(iconDrawable, + null /* user */, + true /* shrinkNonAdaptiveIcons */).icon; + + // Give it a shadow + Bitmap userBadgedBitmap = mBubbleIconFactory.createIconBitmap(mUserBadgedAppIcon, + 1f, mBubbleIconFactory.getBadgeSize()); + Canvas c = new Canvas(); + ShadowGenerator shadowGenerator = new ShadowGenerator(mBubbleIconFactory.getBadgeSize()); + c.setBitmap(userBadgedBitmap); + shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c); + + mBubbleIconFactory.badgeWithDrawable(bubbleIcon, + new BitmapDrawable(mContext.getResources(), userBadgedBitmap)); + mBadgedImageView.setImageBitmap(bubbleIcon); + + // Update badge. int badgeColor = determineDominateColor(iconDrawable, n.color); mBadgeColor = badgeColor; mBadgedImageView.setDotColor(badgeColor); - animateDot(mEntry.showInShadeWhenBubble() /* showDot */, null /* after */); + + // Update dot. + Path iconPath = PathParser.createPathFromPathData( + getResources().getString(com.android.internal.R.string.config_icon_mask)); + Matrix matrix = new Matrix(); + float scale = mBubbleIconFactory.getNormalizer().getScale(iconDrawable, + null /* outBounds */, null /* path */, null /* outMaskShape */); + float radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f; + matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, + radius /* pivot y */); + iconPath.transform(matrix); + mBadgedImageView.drawDot(iconPath); + + animateDot(mBubble.showBubbleDot() /* showDot */, null /* after */); } int getBadgeColor() { return mBadgeColor; } - private Drawable buildIconWithTint(Drawable iconDrawable, int backgroundColor) { + private AdaptiveIconDrawable buildIconWithTint(Drawable iconDrawable, int backgroundColor) { iconDrawable = checkTint(iconDrawable, backgroundColor); InsetDrawable foreground = new InsetDrawable(iconDrawable, mIconInset); ColorDrawable background = new ColorDrawable(backgroundColor); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java index 1fa0e12452e1..c332d15a9b47 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java @@ -16,7 +16,9 @@ package com.android.systemui.bubbles.animation; +import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Path; import android.graphics.Point; import android.graphics.PointF; import android.view.View; @@ -26,10 +28,13 @@ import androidx.annotation.Nullable; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringForce; +import com.android.systemui.Interpolators; import com.android.systemui.R; import com.google.android.collect.Sets; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.Set; /** @@ -46,24 +51,26 @@ public class ExpandedAnimationController */ private static final int ANIMATE_TRANSLATION_FACTOR = 4; - /** How much to scale down bubbles when they're animating in/out. */ - private static final float ANIMATE_SCALE_PERCENT = 0.5f; + /** Duration of the expand/collapse target path animation. */ + private static final int EXPAND_COLLAPSE_TARGET_ANIM_DURATION = 175; - /** The stack position to collapse back to in {@link #collapseBackToStack}. */ - private PointF mCollapseToPoint; + /** Stiffness for the expand/collapse path-following animation. */ + private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000; /** Horizontal offset between bubbles, which we need to know to re-stack them. */ private float mStackOffsetPx; - /** Spacing between bubbles in the expanded state. */ - private float mBubblePaddingPx; + /** Space between status bar and bubbles in the expanded state. */ + private float mBubblePaddingTop; /** Size of each bubble. */ private float mBubbleSizePx; /** Height of the status bar. */ private float mStatusBarHeight; /** Size of display. */ private Point mDisplaySize; - /** Size of dismiss target at bottom of screen. */ - private float mPipDismissHeight; + /** Max number of bubbles shown in row above expanded view.*/ + private int mBubblesMaxRendered; + /** Width of current screen orientation. */ + private float mScreenWidth; /** Whether the dragged-out bubble is in the dismiss target. */ private boolean mIndividualBubbleWithinDismissTarget = false; @@ -86,10 +93,14 @@ public class ExpandedAnimationController private boolean mSpringingBubbleToTouch = false; private int mExpandedViewPadding; + private float mLauncherGridDiff; - public ExpandedAnimationController(Point displaySize, int expandedViewPadding) { + public ExpandedAnimationController(Point displaySize, int expandedViewPadding, + int orientation) { mDisplaySize = displaySize; + updateOrientation(orientation); mExpandedViewPadding = expandedViewPadding; + mLauncherGridDiff = 30f; } /** @@ -109,7 +120,7 @@ public class ExpandedAnimationController mAnimatingExpand = true; mAfterExpand = after; - startOrUpdateExpandAnimation(); + startOrUpdatePathAnimation(true /* expanding */); } /** Animate collapsing the bubbles back to their stacked position. */ @@ -119,43 +130,107 @@ public class ExpandedAnimationController mAfterCollapse = after; mCollapsePoint = collapsePoint; - startOrUpdateCollapseAnimation(); + startOrUpdatePathAnimation(false /* expanding */); } - private void startOrUpdateExpandAnimation() { - animationsForChildrenFromIndex( - 0, /* startIndex */ - (index, animation) -> animation.position(getBubbleLeft(index), getExpandedY())) - .startAll(() -> { - mAnimatingExpand = false; + /** + * Update effective screen width based on current orientation. + * @param orientation Landscape or portrait. + */ + public void updateOrientation(int orientation) { + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + mScreenWidth = mDisplaySize.y; + } else { + mScreenWidth = mDisplaySize.x; + } + if (mLayout != null) { + Resources res = mLayout.getContext().getResources(); + mStatusBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); + } + } - if (mAfterExpand != null) { - mAfterExpand.run(); - } + /** + * Animates the bubbles along a curved path, either to expand them along the top or collapse + * them back into a stack. + */ + private void startOrUpdatePathAnimation(boolean expanding) { + Runnable after; - mAfterExpand = null; - }); - } + if (expanding) { + after = () -> { + mAnimatingExpand = false; - private void startOrUpdateCollapseAnimation() { - // Stack to the left if we're going to the left, or right if not. - final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1; - animationsForChildrenFromIndex( - 0, /* startIndex */ - (index, animation) -> { - animation.position( - mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx), - mCollapsePoint.y); - }) - .startAll(() -> { - mAnimatingCollapse = false; - - if (mAfterCollapse != null) { - mAfterCollapse.run(); - } - - mAfterCollapse = null; - }); + if (mAfterExpand != null) { + mAfterExpand.run(); + } + + mAfterExpand = null; + }; + } else { + after = () -> { + mAnimatingCollapse = false; + + if (mAfterCollapse != null) { + mAfterCollapse.run(); + } + + mAfterCollapse = null; + }; + } + + // Animate each bubble individually, since each path will end in a different spot. + animationsForChildrenFromIndex(0, (index, animation) -> { + final View bubble = mLayout.getChildAt(index); + + // Start a path at the bubble's current position. + final Path path = new Path(); + path.moveTo(bubble.getTranslationX(), bubble.getTranslationY()); + + final float expandedY = getExpandedY(); + if (expanding) { + // If we're expanding, first draw a line from the bubble's current position to the + // top of the screen. + path.lineTo(bubble.getTranslationX(), expandedY); + + // Then, draw a line across the screen to the bubble's resting position. + path.lineTo(getBubbleLeft(index), expandedY); + } else { + final float sideMultiplier = + mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1; + final float stackedX = mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx); + + // If we're collapsing, draw a line from the bubble's current position to the side + // of the screen where the bubble will be stacked. + path.lineTo(stackedX, expandedY); + + // Then, draw a line down to the stack position. + path.lineTo(stackedX, mCollapsePoint.y); + } + + // The lead bubble should be the bubble with the longest distance to travel when we're + // expanding, and the bubble with the shortest distance to travel when we're collapsing. + // During expansion from the left side, the last bubble has to travel to the far right + // side, so we have it lead and 'pull' the rest of the bubbles into place. From the + // right side, the first bubble is traveling to the top left, so it leads. During + // collapse to the left, the first bubble has the shortest travel time back to the stack + // position, so it leads (and vice versa). + final boolean firstBubbleLeads = + (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX())) + || (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x)); + final int startDelay = firstBubbleLeads + ? (index * 10) + : ((mLayout.getChildCount() - index) * 10); + + animation + .followAnimatedTargetAlongPath( + path, + EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */, + Interpolators.LINEAR /* targetAnimInterpolator */) + .withStartDelay(startDelay) + .withStiffness(EXPAND_COLLAPSE_ANIM_STIFFNESS); + }).startAll(after); } /** Prepares the given bubble to be dragged out. */ @@ -265,6 +340,7 @@ public class ExpandedAnimationController public void onGestureFinished() { mBubbleDraggedOutEnough = false; mBubbleDraggingOut = null; + updateBubblePositions(); } /** @@ -276,41 +352,38 @@ public class ExpandedAnimationController 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after); } - /** - * Animates the bubbles, starting at the given index, to the left or right by the given number - * of bubble widths. Passing zero for numBubbleWidths will animate the bubbles to their normal - * positions. - */ - private void animateStackByBubbleWidthsStartingFrom(int numBubbleWidths, int startIndex) { - animationsForChildrenFromIndex( - startIndex, - (index, animation) -> - animation.translationX(getXForChildAtIndex(index + numBubbleWidths))) - .startAll(); - } - /** The Y value of the row of expanded bubbles. */ public float getExpandedY() { if (mLayout == null || mLayout.getRootWindowInsets() == null) { return 0; } final WindowInsets insets = mLayout.getRootWindowInsets(); - return mBubblePaddingPx + Math.max( + return mBubblePaddingTop + Math.max( mStatusBarHeight, insets.getDisplayCutout() != null ? insets.getDisplayCutout().getSafeInsetTop() : 0); } + /** Description of current animation controller state. */ + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("ExpandedAnimationController state:"); + pw.print(" isActive: "); pw.println(isActiveController()); + pw.print(" animatingExpand: "); pw.println(mAnimatingExpand); + pw.print(" animatingCollapse: "); pw.println(mAnimatingCollapse); + pw.print(" bubbleInDismiss: "); pw.println(mIndividualBubbleWithinDismissTarget); + pw.print(" springingBubble: "); pw.println(mSpringingBubbleToTouch); + } + @Override void onActiveControllerForLayout(PhysicsAnimationLayout layout) { final Resources res = layout.getResources(); mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); - mBubblePaddingPx = res.getDimensionPixelSize(R.dimen.bubble_padding); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size); mStatusBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); - mPipDismissHeight = res.getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height); + mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered); // Ensure that all child views are at 1x scale, and visible, in case they were animating // in. @@ -351,11 +424,11 @@ public class ExpandedAnimationController // If a bubble is added while the expand/collapse animations are playing, update the // animation to include the new bubble. if (mAnimatingExpand) { - startOrUpdateExpandAnimation(); + startOrUpdatePathAnimation(true /* expanding */); } else if (mAnimatingCollapse) { - startOrUpdateCollapseAnimation(); + startOrUpdatePathAnimation(false /* expanding */); } else { - child.setTranslationX(getXForChildAtIndex(index)); + child.setTranslationX(getBubbleLeft(index)); animationForChild(child) .translationY( getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */ @@ -389,6 +462,12 @@ public class ExpandedAnimationController @Override void onChildReordered(View child, int oldIndex, int newIndex) { updateBubblePositions(); + + // We expect reordering during collapse, since we'll put the last selected bubble on top. + // Update the collapse animation so they end up in the right stacked positions. + if (mAnimatingCollapse) { + startOrUpdatePathAnimation(false /* expanding */); + } } private void updateBubblePositions() { @@ -411,35 +490,53 @@ public class ExpandedAnimationController } } - /** Returns the appropriate X translation value for a bubble at the given index. */ - private float getXForChildAtIndex(int index) { - return mBubblePaddingPx + (mBubbleSizePx + mBubblePaddingPx) * index; - } - /** * @param index Bubble index in row. * @return Bubble left x from left edge of screen. */ public float getBubbleLeft(int index) { - float bubbleLeftFromRowLeft = index * (mBubbleSizePx + mBubblePaddingPx); - return getRowLeft() + bubbleLeftFromRowLeft; + final float bubbleFromRowLeft = index * (mBubbleSizePx + getSpaceBetweenBubbles()); + return getRowLeft() + bubbleFromRowLeft; } private float getRowLeft() { if (mLayout == null) { return 0; } + int bubbleCount = mLayout.getChildCount(); - // Width calculations. - double bubble = bubbleCount * mBubbleSizePx; - float gap = (bubbleCount - 1) * mBubblePaddingPx; - float row = gap + (float) bubble; + final float totalBubbleWidth = bubbleCount * mBubbleSizePx; + final float totalGapWidth = (bubbleCount - 1) * getSpaceBetweenBubbles(); + final float rowWidth = totalGapWidth + totalBubbleWidth; + + final float centerScreen = mScreenWidth / 2f; + final float halfRow = rowWidth / 2f; + final float rowLeft = centerScreen - halfRow; - float halfRow = row / 2f; - float centerScreen = mDisplaySize.x / 2; - float rowLeftFromScreenLeft = centerScreen - halfRow; + return rowLeft; + } - return rowLeftFromScreenLeft; + /** + * @return Space between bubbles in row above expanded view. + */ + private float getSpaceBetweenBubbles() { + /** + * Ordered left to right: + * Screen edge + * [mExpandedViewPadding] + * Expanded view edge + * [launcherGridDiff] --- arbitrary value until launcher exports widths + * Launcher's app icon grid edge that we must match + */ + final float rowMargins = (mExpandedViewPadding + mLauncherGridDiff) * 2; + final float maxRowWidth = mScreenWidth - rowMargins; + + final float totalBubbleWidth = mBubblesMaxRendered * mBubbleSizePx; + final float totalGapWidth = maxRowWidth - totalBubbleWidth; + + final int gapCount = mBubblesMaxRendered - 1; + final float gapWidth = totalGapWidth / gapCount; + return gapWidth; } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java index 3a3339249d5b..563a0a7e43e1 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java @@ -16,7 +16,14 @@ package com.android.systemui.bubbles.animation; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; import android.content.Context; +import android.graphics.Path; +import android.graphics.PointF; +import android.util.FloatProperty; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -160,7 +167,7 @@ public class PhysicsAnimationLayout extends FrameLayout { /** Whether this controller is the currently active controller for its associated layout. */ protected boolean isActiveController() { - return this == mLayout.mController; + return mLayout != null && this == mLayout.mController; } protected void setLayout(PhysicsAnimationLayout layout) { @@ -232,7 +239,7 @@ public class PhysicsAnimationLayout extends FrameLayout { } if (endActions != null) { - mLayout.setEndActionForMultipleProperties( + setEndActionForMultipleProperties( runAllEndActions, allAnimatedProperties.toArray( new DynamicAnimation.ViewProperty[0])); @@ -243,6 +250,44 @@ public class PhysicsAnimationLayout extends FrameLayout { } }; } + + /** + * Sets an end action that will be run when all child animations for a given property have + * stopped running. + */ + protected void setEndActionForProperty( + Runnable action, DynamicAnimation.ViewProperty property) { + mLayout.mEndActionForProperty.put(property, action); + } + + /** + * Sets an end action that will be run when all child animations for all of the given + * properties have stopped running. + */ + protected void setEndActionForMultipleProperties( + Runnable action, DynamicAnimation.ViewProperty... properties) { + final Runnable checkIfAllFinished = () -> { + if (!mLayout.arePropertiesAnimating(properties)) { + action.run(); + + for (DynamicAnimation.ViewProperty property : properties) { + removeEndActionForProperty(property); + } + } + }; + + for (DynamicAnimation.ViewProperty property : properties) { + setEndActionForProperty(checkIfAllFinished, property); + } + } + + /** + * Removes the end listener that would have been called when all child animations for a + * given property stopped running. + */ + protected void removeEndActionForProperty(DynamicAnimation.ViewProperty property) { + mLayout.mEndActionForProperty.remove(property); + } } /** @@ -275,43 +320,6 @@ public class PhysicsAnimationLayout extends FrameLayout { } } - /** - * Sets an end action that will be run when all child animations for a given property have - * stopped running. - */ - public void setEndActionForProperty(Runnable action, DynamicAnimation.ViewProperty property) { - mEndActionForProperty.put(property, action); - } - - /** - * Sets an end action that will be run when all child animations for all of the given properties - * have stopped running. - */ - public void setEndActionForMultipleProperties( - Runnable action, DynamicAnimation.ViewProperty... properties) { - final Runnable checkIfAllFinished = () -> { - if (!arePropertiesAnimating(properties)) { - action.run(); - - for (DynamicAnimation.ViewProperty property : properties) { - removeEndActionForProperty(property); - } - } - }; - - for (DynamicAnimation.ViewProperty property : properties) { - setEndActionForProperty(checkIfAllFinished, property); - } - } - - /** - * Removes the end listener that would have been called when all child animations for a given - * property stopped running. - */ - public void removeEndActionForProperty(DynamicAnimation.ViewProperty property) { - mEndActionForProperty.remove(property); - } - @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { addViewInternal(child, index, params, false /* isReorder */); @@ -372,11 +380,22 @@ public class PhysicsAnimationLayout extends FrameLayout { /** Checks whether any animations of the given properties are running on the given view. */ public boolean arePropertiesAnimatingOnView( View view, DynamicAnimation.ViewProperty... properties) { + final ObjectAnimator targetAnimator = getTargetAnimatorFromView(view); for (DynamicAnimation.ViewProperty property : properties) { final SpringAnimation animation = getAnimationFromView(property, view); if (animation != null && animation.isRunning()) { return true; } + + // If the target animator is running, its update listener will trigger the translation + // physics animations at some point. We should consider the translation properties to be + // be animating in this case, even if the physics animations haven't been started yet. + final boolean isTranslation = + property.equals(DynamicAnimation.TRANSLATION_X) + || property.equals(DynamicAnimation.TRANSLATION_Y); + if (isTranslation && targetAnimator != null && targetAnimator.isRunning()) { + return true; + } } return false; @@ -388,8 +407,18 @@ public class PhysicsAnimationLayout extends FrameLayout { return; } + cancelAllAnimationsOfProperties( + mController.getAnimatedProperties().toArray(new DynamicAnimation.ViewProperty[]{})); + } + + /** Cancels all animations that are running on all child views, for the given properties. */ + public void cancelAllAnimationsOfProperties(DynamicAnimation.ViewProperty... properties) { + if (mController == null) { + return; + } + for (int i = 0; i < getChildCount(); i++) { - for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { + for (DynamicAnimation.ViewProperty property : properties) { final DynamicAnimation anim = getAnimationAtIndex(property, i); if (anim != null) { anim.cancel(); @@ -400,6 +429,14 @@ public class PhysicsAnimationLayout extends FrameLayout { /** Cancels all of the physics animations running on the given view. */ public void cancelAnimationsOnView(View view) { + // If present, cancel the target animator so it doesn't restart the translation physics + // animations. + final ObjectAnimator targetAnimator = getTargetAnimatorFromView(view); + if (targetAnimator != null) { + targetAnimator.cancel(); + } + + // Cancel physics animations on the view. for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { getAnimationFromView(property, view).cancel(); } @@ -470,6 +507,11 @@ public class PhysicsAnimationLayout extends FrameLayout { return (SpringAnimation) view.getTag(getTagIdForProperty(property)); } + /** Retrieves the target animator from the view via the view tag system. */ + @Nullable private ObjectAnimator getTargetAnimatorFromView(View view) { + return (ObjectAnimator) view.getTag(R.id.target_animator_tag); + } + /** Sets up SpringAnimations of the given property for each child view in the layout. */ private void setUpAnimationsForProperty(DynamicAnimation.ViewProperty property) { for (int i = 0; i < getChildCount(); i++) { @@ -587,7 +629,7 @@ public class PhysicsAnimationLayout extends FrameLayout { * End actions to call when both TRANSLATION_X and TRANSLATION_Y animations have completed, * if {@link #position} was used to animate TRANSLATION_X and TRANSLATION_Y simultaneously. */ - private Runnable[] mPositionEndActions; + @Nullable private Runnable[] mPositionEndActions; /** * All of the properties that have been set and will animate when {@link #start} is called. @@ -603,6 +645,46 @@ public class PhysicsAnimationLayout extends FrameLayout { /** The animation controller that last retrieved this animator instance. */ private PhysicsAnimationController mAssociatedController; + /** + * Animator used to traverse the path provided to {@link #followAnimatedTargetAlongPath}. As + * the path is traversed, the view's translation spring animation final positions are + * updated such that the view 'follows' the current position on the path. + */ + @Nullable private ObjectAnimator mPathAnimator; + + /** Current position on the path. This is animated by {@link #mPathAnimator}. */ + private PointF mCurrentPointOnPath = new PointF(); + + /** + * FloatProperty instances that can be passed to {@link ObjectAnimator} to animate the value + * of {@link #mCurrentPointOnPath}. + */ + private final FloatProperty<PhysicsPropertyAnimator> mCurrentPointOnPathXProperty = + new FloatProperty<PhysicsPropertyAnimator>("PathX") { + @Override + public void setValue(PhysicsPropertyAnimator object, float value) { + mCurrentPointOnPath.x = value; + } + + @Override + public Float get(PhysicsPropertyAnimator object) { + return mCurrentPointOnPath.x; + } + }; + + private final FloatProperty<PhysicsPropertyAnimator> mCurrentPointOnPathYProperty = + new FloatProperty<PhysicsPropertyAnimator>("PathY") { + @Override + public void setValue(PhysicsPropertyAnimator object, float value) { + mCurrentPointOnPath.y = value; + } + + @Override + public Float get(PhysicsPropertyAnimator object) { + return mCurrentPointOnPath.y; + } + }; + protected PhysicsPropertyAnimator(View view) { this.mView = view; } @@ -628,6 +710,7 @@ public class PhysicsAnimationLayout extends FrameLayout { /** Animate the view's translationX value to the provided value. */ public PhysicsPropertyAnimator translationX(float translationX, Runnable... endActions) { + mPathAnimator = null; // We aren't using the path anymore if we're translating. return property(DynamicAnimation.TRANSLATION_X, translationX, endActions); } @@ -640,6 +723,7 @@ public class PhysicsAnimationLayout extends FrameLayout { /** Animate the view's translationY value to the provided value. */ public PhysicsPropertyAnimator translationY(float translationY, Runnable... endActions) { + mPathAnimator = null; // We aren't using the path anymore if we're translating. return property(DynamicAnimation.TRANSLATION_Y, translationY, endActions); } @@ -661,6 +745,46 @@ public class PhysicsAnimationLayout extends FrameLayout { return translationY(translationY); } + /** + * Animates a 'target' point that moves along the given path, using the provided duration + * and interpolator to animate the target. The view itself is animated using physics-based + * animations, whose final positions are updated to the target position as it animates. This + * results in the view 'following' the target in a realistic way. + * + * This method will override earlier calls to {@link #translationX}, {@link #translationY}, + * or {@link #position}, ultimately animating the view's position to the final point on the + * given path. + * + * Any provided end listeners will be called when the physics-based animations kicked off by + * the moving target have completed - not when the target animation completes. + */ + public PhysicsPropertyAnimator followAnimatedTargetAlongPath( + Path path, + int targetAnimDuration, + TimeInterpolator targetAnimInterpolator, + Runnable... endActions) { + mPathAnimator = ObjectAnimator.ofFloat( + this, mCurrentPointOnPathXProperty, mCurrentPointOnPathYProperty, path); + mPathAnimator.setDuration(targetAnimDuration); + mPathAnimator.setInterpolator(targetAnimInterpolator); + + mPositionEndActions = endActions; + + // Remove translation related values since we're going to ignore them and follow the + // path instead. + clearTranslationValues(); + return this; + } + + private void clearTranslationValues() { + mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_X); + mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_Y); + mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_X); + mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_Y); + mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_X); + mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_Y); + } + /** Animate the view's scaleX value to the provided value. */ public PhysicsPropertyAnimator scaleX(float scaleX, Runnable... endActions) { return property(DynamicAnimation.SCALE_X, scaleX, endActions); @@ -742,7 +866,7 @@ public class PhysicsAnimationLayout extends FrameLayout { if (after != null && after.length > 0) { final DynamicAnimation.ViewProperty[] propertiesArray = properties.toArray(new DynamicAnimation.ViewProperty[0]); - setEndActionForMultipleProperties(() -> { + mAssociatedController.setEndActionForMultipleProperties(() -> { for (Runnable callback : after) { callback.run(); } @@ -774,8 +898,20 @@ public class PhysicsAnimationLayout extends FrameLayout { new Runnable[]{waitForBothXAndY}); } + if (mPathAnimator != null) { + startPathAnimation(); + } + // Actually start the animations. for (DynamicAnimation.ViewProperty property : properties) { + // Don't start translation animations if we're using a path animator, the update + // listeners added to that animator will take care of that. + if (mPathAnimator != null + && (property.equals(DynamicAnimation.TRANSLATION_X) + || property.equals(DynamicAnimation.TRANSLATION_Y))) { + return; + } + if (mInitialPropertyValues.containsKey(property)) { property.setValue(mView, mInitialPropertyValues.get(property)); } @@ -797,7 +933,16 @@ public class PhysicsAnimationLayout extends FrameLayout { /** Returns the set of properties that will animate once {@link #start} is called. */ protected Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { - return mAnimatedProperties.keySet(); + final HashSet<DynamicAnimation.ViewProperty> animatedProperties = new HashSet<>( + mAnimatedProperties.keySet()); + + // If we're using a path animator, it'll kick off translation animations. + if (mPathAnimator != null) { + animatedProperties.add(DynamicAnimation.TRANSLATION_X); + animatedProperties.add(DynamicAnimation.TRANSLATION_Y); + } + + return animatedProperties; } /** @@ -812,7 +957,7 @@ public class PhysicsAnimationLayout extends FrameLayout { long startDelay, float stiffness, float dampingRatio, - Runnable[] afterCallbacks) { + Runnable... afterCallbacks) { if (view != null) { final SpringAnimation animation = (SpringAnimation) view.getTag(getTagIdForProperty(property)); @@ -855,6 +1000,92 @@ public class PhysicsAnimationLayout extends FrameLayout { } } + /** + * Updates the final position of a view's animation, without changing any of the animation's + * other settings. Calling this before an initial call to {@link #animateValueForChild} will + * work, but result in unknown values for stiffness, etc. and is not recommended. + */ + private void updateValueForChild( + DynamicAnimation.ViewProperty property, View view, float position) { + if (view != null) { + final SpringAnimation animation = + (SpringAnimation) view.getTag(getTagIdForProperty(property)); + final SpringForce animationSpring = animation.getSpring(); + + if (animationSpring == null) { + return; + } + + animationSpring.setFinalPosition(position); + animation.start(); + } + } + + /** + * Configures the path animator to respect the settings passed into the animation builder + * and adds update listeners that update the translation physics animations. Then, starts + * the path animation. + */ + protected void startPathAnimation() { + final SpringForce defaultSpringForceX = mController.getSpringForce( + DynamicAnimation.TRANSLATION_X, mView); + final SpringForce defaultSpringForceY = mController.getSpringForce( + DynamicAnimation.TRANSLATION_Y, mView); + + if (mStartDelay > 0) { + mPathAnimator.setStartDelay(mStartDelay); + } + + final Runnable updatePhysicsAnims = () -> { + updateValueForChild( + DynamicAnimation.TRANSLATION_X, mView, mCurrentPointOnPath.x); + updateValueForChild( + DynamicAnimation.TRANSLATION_Y, mView, mCurrentPointOnPath.y); + }; + + mPathAnimator.addUpdateListener(pathAnim -> updatePhysicsAnims.run()); + mPathAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + animateValueForChild( + DynamicAnimation.TRANSLATION_X, + mView, + mCurrentPointOnPath.x, + mDefaultStartVelocity, + 0 /* startDelay */, + mStiffness >= 0 ? mStiffness : defaultSpringForceX.getStiffness(), + mDampingRatio >= 0 + ? mDampingRatio + : defaultSpringForceX.getDampingRatio()); + + animateValueForChild( + DynamicAnimation.TRANSLATION_Y, + mView, + mCurrentPointOnPath.y, + mDefaultStartVelocity, + 0 /* startDelay */, + mStiffness >= 0 ? mStiffness : defaultSpringForceY.getStiffness(), + mDampingRatio >= 0 + ? mDampingRatio + : defaultSpringForceY.getDampingRatio()); + } + + @Override + public void onAnimationEnd(Animator animation) { + updatePhysicsAnims.run(); + } + }); + + // If there's a target animator saved for the view, make sure it's not running. + final ObjectAnimator targetAnimator = getTargetAnimatorFromView(mView); + if (targetAnimator != null) { + targetAnimator.cancel(); + } + + mView.setTag(R.id.target_animator_tag, mPathAnimator); + mPathAnimator.start(); + } + private void clearAnimator() { mInitialPropertyValues.clear(); mAnimatedProperties.clear(); @@ -864,6 +1095,8 @@ public class PhysicsAnimationLayout extends FrameLayout { mStiffness = -1; mDampingRatio = -1; mEndActionsForProperty.clear(); + mPathAnimator = null; + mPositionEndActions = null; } /** diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java index ab8752e4195f..2ec09a9dbea2 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java @@ -23,6 +23,7 @@ import android.util.Log; import android.view.View; import android.view.WindowInsets; +import androidx.annotation.Nullable; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.FlingAnimation; import androidx.dynamicanimation.animation.FloatPropertyCompat; @@ -33,6 +34,8 @@ import com.android.systemui.R; import com.google.android.collect.Sets; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.HashMap; import java.util.Set; @@ -53,6 +56,10 @@ public class StackAnimationController extends /** Translation factor (multiplied by stack offset) to use for bubbles being animated in/out. */ private static final int ANIMATE_TRANSLATION_FACTOR = 4; + /** Values to use for animating bubbles in. */ + private static final float ANIMATE_IN_STIFFNESS = 1000f; + private static final int ANIMATE_IN_START_DELAY = 25; + /** * Values to use for the default {@link SpringForce} provided to the physics animation layout. */ @@ -92,7 +99,7 @@ public class StackAnimationController extends private boolean mStackMovedToStartPosition = false; /** The most recent position in which the stack was resting on the edge of the screen. */ - private PointF mRestingStackPosition; + @Nullable private PointF mRestingStackPosition; /** The height of the most recently visible IME. */ private float mImeHeight = 0f; @@ -139,14 +146,16 @@ public class StackAnimationController extends /** Horizontal offset of bubbles in the stack. */ private float mStackOffset; - /** Diameter of the bubbles themselves. */ - private int mIndividualBubbleSize; + /** Diameter of the bubble icon. */ + private int mBubbleIconBitmapSize; + /** Width of the bubble (icon and padding). */ + private int mBubbleSize; /** * The amount of space to add between the bubbles and certain UI elements, such as the top of * the screen or the IME. This does not apply to the left/right sides of the screen since the * stack goes offscreen intentionally. */ - private int mBubblePadding; + private int mBubblePaddingTop; /** How far offscreen the stack rests. */ private int mBubbleOffscreen; /** How far down the screen the stack starts, when there is no pre-existing location. */ @@ -185,7 +194,7 @@ public class StackAnimationController extends return false; } - float stackCenter = mStackPosition.x + mIndividualBubbleSize / 2; + float stackCenter = mStackPosition.x + mBubbleIconBitmapSize / 2; float screenCenter = mLayout.getWidth() / 2; return stackCenter < screenCenter; } @@ -197,18 +206,18 @@ public class StackAnimationController extends */ public void springStack(float destinationX, float destinationY) { springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, - new SpringForce() + new SpringForce() .setStiffness(SPRING_AFTER_FLING_STIFFNESS) .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), - 0 /* startXVelocity */, - destinationX); + 0 /* startXVelocity */, + destinationX); springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, - new SpringForce() + new SpringForce() .setStiffness(SPRING_AFTER_FLING_STIFFNESS) .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), - 0 /* startYVelocity */, - destinationY); + 0 /* startYVelocity */, + destinationY); } /** @@ -218,7 +227,7 @@ public class StackAnimationController extends * @return The X value that the stack will end up at after the fling/spring. */ public float flingStackThenSpringToEdge(float x, float velX, float velY) { - final boolean stackOnLeftSide = x - mIndividualBubbleSize / 2 < mLayout.getWidth() / 2; + final boolean stackOnLeftSide = x - mBubbleIconBitmapSize / 2 < mLayout.getWidth() / 2; final boolean stackShouldFlingLeft = stackOnLeftSide ? velX < ESCAPE_VELOCITY @@ -230,6 +239,12 @@ public class StackAnimationController extends final float destinationRelativeX = stackShouldFlingLeft ? stackBounds.left : stackBounds.right; + // If all bubbles were removed during a drag event, just return the X we would have animated + // to if there were still bubbles. + if (mLayout == null || mLayout.getChildCount() == 0) { + return destinationRelativeX; + } + // Minimum velocity required for the stack to make it to the targeted side of the screen, // taking friction into account (4.2f is the number that friction scalars are multiplied by // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off, @@ -262,15 +277,6 @@ public class StackAnimationController extends .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), /* destination */ null); - mLayout.setEndActionForMultipleProperties( - () -> { - mRestingStackPosition = new PointF(); - mRestingStackPosition.set(mStackPosition); - mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X); - mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y); - }, - DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); - // If we're flinging now, there's no more touch event to catch up to. mFirstBubbleSpringingToTouch = false; mIsMovingFromFlinging = true; @@ -304,6 +310,18 @@ public class StackAnimationController extends setStackPosition(new PointF(x, y)); } + /** Description of current animation controller state. */ + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("StackAnimationController state:"); + pw.print(" isActive: "); pw.println(isActiveController()); + pw.print(" restingStackPos: "); + pw.println(mRestingStackPosition != null ? mRestingStackPosition.toString() : "null"); + pw.print(" currentStackPos: "); pw.println(mStackPosition.toString()); + pw.print(" isMovingFromFlinging: "); pw.println(mIsMovingFromFlinging); + pw.print(" withinDismiss: "); pw.println(mWithinDismissTarget); + pw.print(" firstBubbleSpringing: "); pw.println(mFirstBubbleSpringingToTouch); + } + /** * Flings the first bubble along the given property's axis, using the provided configuration * values. When the animation ends - either by hitting the min/max, or by friction sufficiently @@ -317,7 +335,7 @@ public class StackAnimationController extends SpringForce spring, Float finalPosition) { Log.d(TAG, String.format("Flinging %s.", - PhysicsAnimationLayout.getReadablePropertyName(property))); + PhysicsAnimationLayout.getReadablePropertyName(property))); StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); final float currentValue = firstBubbleProperty.getValue(this); @@ -347,6 +365,9 @@ public class StackAnimationController extends .addEndListener((animation, canceled, endValue, endVelocity) -> { if (!canceled) { + mRestingStackPosition = new PointF(); + mRestingStackPosition.set(mStackPosition); + springFirstBubbleWithStackFollowing(property, spring, endVelocity, finalPosition != null ? finalPosition @@ -368,8 +389,8 @@ public class StackAnimationController extends cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X); cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y); - mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X); - mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y); + removeEndActionForProperty(DynamicAnimation.TRANSLATION_X); + removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y); } /** Save the current IME height so that we know where the stack bounds should be. */ @@ -427,7 +448,7 @@ public class StackAnimationController extends : 0); allowableRegion.right = mLayout.getWidth() - - mIndividualBubbleSize + - mBubbleSize + mBubbleOffscreen - Math.max( insets.getSystemWindowInsetRight(), @@ -436,7 +457,7 @@ public class StackAnimationController extends : 0); allowableRegion.top = - mBubblePadding + mBubblePaddingTop + Math.max( mStatusBarHeight, insets.getDisplayCutout() != null @@ -444,9 +465,9 @@ public class StackAnimationController extends : 0); allowableRegion.bottom = mLayout.getHeight() - - mIndividualBubbleSize - - mBubblePadding - - (mImeHeight > Float.MIN_VALUE ? mImeHeight + mBubblePadding : 0f) + - mBubbleSize + - mBubblePaddingTop + - (mImeHeight > Float.MIN_VALUE ? mImeHeight + mBubblePaddingTop : 0f) - Math.max( insets.getSystemWindowInsetBottom(), insets.getDisplayCutout() != null @@ -516,13 +537,19 @@ public class StackAnimationController extends mWithinDismissTarget = true; mFirstBubbleSpringingToTouch = false; - animationForChildAtIndex(0) - .translationX(mLayout.getWidth() / 2f - mIndividualBubbleSize / 2f) - .translationY(destY, after) - .withPositionStartVelocities(velX, velY) - .withStiffness(SpringForce.STIFFNESS_MEDIUM) - .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) - .start(); + springFirstBubbleWithStackFollowing( + DynamicAnimation.TRANSLATION_X, + new SpringForce() + .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) + .setStiffness(SpringForce.STIFFNESS_MEDIUM), + velX, mLayout.getWidth() / 2f - mBubbleIconBitmapSize / 2f); + + springFirstBubbleWithStackFollowing( + DynamicAnimation.TRANSLATION_Y, + new SpringForce() + .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) + .setStiffness(SpringForce.STIFFNESS_MEDIUM), + velY, destY, after); } /** @@ -550,7 +577,7 @@ public class StackAnimationController extends */ protected void springFirstBubbleWithStackFollowing( DynamicAnimation.ViewProperty property, SpringForce spring, - float vel, float finalPosition) { + float vel, float finalPosition, @Nullable Runnable... after) { if (mLayout.getChildCount() == 0) { return; @@ -564,6 +591,13 @@ public class StackAnimationController extends SpringAnimation springAnimation = new SpringAnimation(this, firstBubbleProperty) .setSpring(spring) + .addEndListener((dynamicAnimation, b, v, v1) -> { + if (after != null) { + for (Runnable callback : after) { + callback.run(); + } + } + }) .setStartVelocity(vel); cancelStackPositionAnimation(property); @@ -620,13 +654,18 @@ public class StackAnimationController extends @Override void onChildAdded(View child, int index) { + // Don't animate additions within the dismiss target. + if (mWithinDismissTarget) { + return; + } + if (mLayout.getChildCount() == 1) { // If this is the first child added, position the stack in its starting position. moveStackToStartPosition(); } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) { // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble // to the back of the stack, it'll be largely invisible so don't bother animating it in. - animateInBubble(child); + animateInBubble(child, index); } } @@ -641,24 +680,29 @@ public class StackAnimationController extends .translationX(mStackPosition.x - (-xOffset * ANIMATE_TRANSLATION_FACTOR)) .start(); + // If there are other bubbles, pull them into the correct position. if (mLayout.getChildCount() > 0) { animationForChildAtIndex(0).translationX(mStackPosition.x).start(); } else { - // Set the start position back to the default since we're out of bubbles. New bubbles - // will then animate in from the start position. - mStackPosition = getDefaultStartPosition(); + // If there's no other bubbles, and we were in the dismiss target, reset the flag. + mWithinDismissTarget = false; } } @Override - void onChildReordered(View child, int oldIndex, int newIndex) {} + void onChildReordered(View child, int oldIndex, int newIndex) { + if (isStackPositionSet()) { + setStackPosition(mStackPosition); + } + } @Override void onActiveControllerForLayout(PhysicsAnimationLayout layout) { Resources res = layout.getResources(); mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); - mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); - mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding); + mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); + mBubbleIconBitmapSize = res.getDimensionPixelSize(R.dimen.bubble_icon_bitmap_size); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); mStackStartingVerticalOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y); @@ -666,6 +710,20 @@ public class StackAnimationController extends res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); } + /** + * Update effective screen width based on current orientation. + * @param orientation Landscape or portrait. + */ + public void updateOrientation(int orientation) { + if (mLayout != null) { + Resources res = mLayout.getContext().getResources(); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); + mStatusBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + } + } + + /** Moves the stack, without any animation, to the starting position. */ private void moveStackToStartPosition() { // Post to ensure that the layout's width and height have been calculated. @@ -679,7 +737,7 @@ public class StackAnimationController extends // Animate in the top bubble now that we're visible. if (mLayout.getChildCount() > 0) { - animateInBubble(mLayout.getChildAt(0)); + animateInBubble(mLayout.getChildAt(0), 0 /* index */); } }); } @@ -715,7 +773,9 @@ public class StackAnimationController extends // If we're not the active controller, we don't want to physically move the bubble views. if (isActiveController()) { - mLayout.cancelAllAnimations(); + // Cancel animations that could be moving the views. + mLayout.cancelAllAnimationsOfProperties( + DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); cancelStackPositionAnimations(); // Since we're not using the chained animations, apply the offsets manually. @@ -742,21 +802,34 @@ public class StackAnimationController extends } /** Animates in the given bubble. */ - private void animateInBubble(View child) { + private void animateInBubble(View child, int index) { if (!isActiveController()) { return; } + final float xOffset = + getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X); + + // Position the new bubble in the correct position, scaled down completely. + child.setTranslationX(mStackPosition.x + xOffset * index); child.setTranslationY(mStackPosition.y); + child.setScaleX(0f); + child.setScaleY(0f); + + // Push the subsequent views out of the way, if there are subsequent views. + if (index + 1 < mLayout.getChildCount()) { + animationForChildAtIndex(index + 1) + .translationX(mStackPosition.x + xOffset * (index + 1)) + .withStiffness(SpringForce.STIFFNESS_LOW) + .start(); + } - float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X); + // Scale in the new bubble, slightly delayed. animationForChild(child) - .scaleX(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */) - .scaleY(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */) - .alpha(0f /* from */, 1f /* to */) - .translationX( - mStackPosition.x - ANIMATE_TRANSLATION_FACTOR * xOffset /* from */, - mStackPosition.x /* to */) + .scaleX(1f) + .scaleY(1f) + .withStiffness(ANIMATE_IN_STIFFNESS) + .withStartDelay(mLayout.getChildCount() > 1 ? ANIMATE_IN_START_DELAY : 0) .start(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index 6e75c0375afc..66a06193d0bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -28,7 +28,7 @@ import android.view.View; import android.view.ViewGroup; import com.android.systemui.R; -import com.android.systemui.bubbles.BubbleData; +import com.android.systemui.bubbles.BubbleController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationEntryManager; @@ -85,7 +85,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle * possible. */ private final boolean mAlwaysExpandNonGroupedNotification; - private final BubbleData mBubbleData; + private final BubbleController mBubbleController; private final DynamicPrivacyController mDynamicPrivacyController; private final KeyguardBypassController mBypassController; @@ -107,8 +107,8 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle StatusBarStateController statusBarStateController, NotificationEntryManager notificationEntryManager, Lazy<ShadeController> shadeController, - BubbleData bubbleData, KeyguardBypassController bypassController, + BubbleController bubbleController, DynamicPrivacyController privacyController) { mHandler = mainHandler; mLockscreenUserManager = notificationLockscreenUserManager; @@ -121,7 +121,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle Resources res = context.getResources(); mAlwaysExpandNonGroupedNotification = res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications); - mBubbleData = bubbleData; + mBubbleController = bubbleController; mDynamicPrivacyController = privacyController; privacyController.addListener(this); } @@ -147,7 +147,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle for (int i = 0; i < N; i++) { NotificationEntry ent = activeNotifications.get(i); if (ent.isRowDismissed() || ent.isRowRemoved() - || (mBubbleData.hasBubbleWithKey(ent.key) && !ent.showInShadeWhenBubble())) { + || mBubbleController.isBubbleNotificationSuppressedFromShade(ent.key)) { // we don't want to update removed notifications because they could // temporarily become children if they were isolated before. continue; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java index 154d7b356cd1..f99662e69b8e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java @@ -133,11 +133,6 @@ public class NotificationFilter { } } } - - if (entry.isBubble() && !entry.showInShadeWhenBubble()) { - return true; - } - return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java index 68d95463bd3a..150667b86828 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java @@ -39,7 +39,6 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.policy.HeadsUpManager; import javax.inject.Inject; @@ -57,9 +56,8 @@ public class NotificationInterruptionStateProvider { private static final boolean ENABLE_HEADS_UP = true; private static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up"; - private final StatusBarStateController mStatusBarStateController = - Dependency.get(StatusBarStateController.class); - private final NotificationFilter mNotificationFilter = Dependency.get(NotificationFilter.class); + private final StatusBarStateController mStatusBarStateController; + private final NotificationFilter mNotificationFilter; private final AmbientDisplayConfiguration mAmbientDisplayConfiguration; private final Context mContext; @@ -67,7 +65,6 @@ public class NotificationInterruptionStateProvider { private final IDreamManager mDreamManager; private NotificationPresenter mPresenter; - private ShadeController mShadeController; private HeadsUpManager mHeadsUpManager; private HeadsUpSuppressor mHeadsUpSuppressor; @@ -77,12 +74,15 @@ public class NotificationInterruptionStateProvider { private boolean mDisableNotificationAlerts; @Inject - public NotificationInterruptionStateProvider(Context context) { + public NotificationInterruptionStateProvider(Context context, NotificationFilter filter, + StatusBarStateController stateController) { this(context, (PowerManager) context.getSystemService(Context.POWER_SERVICE), IDreamManager.Stub.asInterface( ServiceManager.checkService(DreamService.DREAM_SERVICE)), - new AmbientDisplayConfiguration(context)); + new AmbientDisplayConfiguration(context), + filter, + stateController); } @VisibleForTesting @@ -90,11 +90,15 @@ public class NotificationInterruptionStateProvider { Context context, PowerManager powerManager, IDreamManager dreamManager, - AmbientDisplayConfiguration ambientDisplayConfiguration) { + AmbientDisplayConfiguration ambientDisplayConfiguration, + NotificationFilter notificationFilter, + StatusBarStateController statusBarStateController) { mContext = context; mPowerManager = powerManager; mDreamManager = dreamManager; mAmbientDisplayConfiguration = ambientDisplayConfiguration; + mNotificationFilter = notificationFilter; + mStatusBarStateController = statusBarStateController; } /** Sets up late-binding dependencies for this component. */ @@ -102,29 +106,39 @@ public class NotificationInterruptionStateProvider { NotificationPresenter notificationPresenter, HeadsUpManager headsUpManager, HeadsUpSuppressor headsUpSuppressor) { + setUpWithPresenter(notificationPresenter, headsUpManager, headsUpSuppressor, + new ContentObserver(Dependency.get(Dependency.MAIN_HANDLER)) { + @Override + public void onChange(boolean selfChange) { + boolean wasUsing = mUseHeadsUp; + mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts + && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt( + mContext.getContentResolver(), + Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, + Settings.Global.HEADS_UP_OFF); + Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled")); + if (wasUsing != mUseHeadsUp) { + if (!mUseHeadsUp) { + Log.d(TAG, + "dismissing any existing heads up notification on disable" + + " event"); + mHeadsUpManager.releaseAllImmediately(); + } + } + } + }); + } + + /** Sets up late-binding dependencies for this component. */ + public void setUpWithPresenter( + NotificationPresenter notificationPresenter, + HeadsUpManager headsUpManager, + HeadsUpSuppressor headsUpSuppressor, + ContentObserver observer) { mPresenter = notificationPresenter; mHeadsUpManager = headsUpManager; mHeadsUpSuppressor = headsUpSuppressor; - - mHeadsUpObserver = new ContentObserver(Dependency.get(Dependency.MAIN_HANDLER)) { - @Override - public void onChange(boolean selfChange) { - boolean wasUsing = mUseHeadsUp; - mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts - && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt( - mContext.getContentResolver(), - Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, - Settings.Global.HEADS_UP_OFF); - Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled")); - if (wasUsing != mUseHeadsUp) { - if (!mUseHeadsUp) { - Log.d(TAG, - "dismissing any existing heads up notification on disable event"); - mHeadsUpManager.releaseAllImmediately(); - } - } - } - }; + mHeadsUpObserver = observer; if (ENABLE_HEADS_UP) { mContext.getContentResolver().registerContentObserver( @@ -138,13 +152,6 @@ public class NotificationInterruptionStateProvider { mHeadsUpObserver.onChange(true); // set up } - private ShadeController getShadeController() { - if (mShadeController == null) { - mShadeController = Dependency.get(ShadeController.class); - } - return mShadeController; - } - /** * Whether the notification should appear as a bubble with a fly-out on top of the screen. * @@ -153,6 +160,15 @@ public class NotificationInterruptionStateProvider { */ public boolean shouldBubbleUp(NotificationEntry entry) { final StatusBarNotification sbn = entry.notification; + + if (!canAlertCommon(entry)) { + return false; + } + + if (!canAlertAwakeCommon(entry)) { + return false; + } + if (!entry.canBubble) { if (DEBUG) { Log.d(TAG, "No bubble up: not allowed to bubble: " + sbn.getKey()); @@ -177,10 +193,6 @@ public class NotificationInterruptionStateProvider { return false; } - if (!canHeadsUpCommon(entry)) { - return false; - } - return true; } @@ -201,23 +213,34 @@ public class NotificationInterruptionStateProvider { private boolean shouldHeadsUpWhenAwake(NotificationEntry entry) { StatusBarNotification sbn = entry.notification; - boolean inShade = mStatusBarStateController.getState() == SHADE; - if (entry.isBubble() && inShade) { + if (!mUseHeadsUp) { if (DEBUG_HEADS_UP) { - Log.d(TAG, "No heads up: in unlocked shade where notification is shown as a " - + "bubble: " + sbn.getKey()); + Log.d(TAG, "No heads up: no huns"); } return false; } if (!canAlertCommon(entry)) { + return false; + } + + if (!canAlertAwakeCommon(entry)) { + return false; + } + + boolean inShade = mStatusBarStateController.getState() == SHADE; + if (entry.isBubble() && inShade) { if (DEBUG_HEADS_UP) { - Log.d(TAG, "No heads up: notification shouldn't alert: " + sbn.getKey()); + Log.d(TAG, "No heads up: in unlocked shade where notification is shown as a " + + "bubble: " + sbn.getKey()); } return false; } - if (!canHeadsUpCommon(entry)) { + if (entry.shouldSuppressPeek()) { + if (DEBUG_HEADS_UP) { + Log.d(TAG, "No heads up: suppressed by DND: " + sbn.getKey()); + } return false; } @@ -294,16 +317,13 @@ public class NotificationInterruptionStateProvider { } /** - * Common checks between regular heads up and when pulsing. See - * {@link #shouldHeadsUp(NotificationEntry)} and - * {@link #shouldHeadsUpWhenDozing(NotificationEntry)}. Notifications that fail any of these - * checks - * should not alert at all. + * Common checks between regular & AOD heads up and bubbles. * * @param entry the entry to check * @return true if these checks pass, false if the notification should not alert */ - protected boolean canAlertCommon(NotificationEntry entry) { + @VisibleForTesting + public boolean canAlertCommon(NotificationEntry entry) { StatusBarNotification sbn = entry.notification; if (mNotificationFilter.shouldFilterOut(entry)) { @@ -320,46 +340,36 @@ public class NotificationInterruptionStateProvider { } return false; } - return true; } /** - * Common checks between heads up alerting and bubble fly out alerting. See - * {@link #shouldHeadsUp(NotificationEntry)} and - * {@link #shouldBubbleUp(NotificationEntry)}. Notifications that fail any of these - * checks should not interrupt the user on screen. + * Common checks between alerts that occur while the device is awake (heads up & bubbles). * * @param entry the entry to check - * @return true if these checks pass, false if the notification should not interrupt on screen + * @return true if these checks pass, false if the notification should not alert */ - public boolean canHeadsUpCommon(NotificationEntry entry) { + @VisibleForTesting + public boolean canAlertAwakeCommon(NotificationEntry entry) { StatusBarNotification sbn = entry.notification; - if (!mUseHeadsUp || mPresenter.isDeviceInVrMode()) { - if (DEBUG_HEADS_UP) { - Log.d(TAG, "No heads up: no huns or vr mode"); - } - return false; - } - - if (entry.shouldSuppressPeek()) { + if (mPresenter.isDeviceInVrMode()) { if (DEBUG_HEADS_UP) { - Log.d(TAG, "No heads up: suppressed by DND: " + sbn.getKey()); + Log.d(TAG, "No alerting: no huns or vr mode"); } return false; } if (isSnoozedPackage(sbn)) { if (DEBUG_HEADS_UP) { - Log.d(TAG, "No heads up: snoozed package: " + sbn.getKey()); + Log.d(TAG, "No alerting: snoozed package: " + sbn.getKey()); } return false; } if (entry.hasJustLaunchedFullScreenIntent()) { if (DEBUG_HEADS_UP) { - Log.d(TAG, "No heads up: recent fullscreen: " + sbn.getKey()); + Log.d(TAG, "No alerting: recent fullscreen: " + sbn.getKey()); } return false; } @@ -377,6 +387,18 @@ public class NotificationInterruptionStateProvider { mHeadsUpObserver.onChange(true); } + /** Whether all alerts are disabled. */ + @VisibleForTesting + public boolean areNotificationAlertsDisabled() { + return mDisableNotificationAlerts; + } + + /** Whether HUNs should be used. */ + @VisibleForTesting + public boolean getUseHeadsUp() { + return mUseHeadsUp; + } + protected NotificationPresenter getPresenter() { return mPresenter; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 6178488cf43a..2964889f1399 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -23,6 +23,7 @@ import static android.app.Notification.CATEGORY_MESSAGE; import static android.app.Notification.CATEGORY_REMINDER; import static android.app.Notification.FLAG_BUBBLE; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; +import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; @@ -41,7 +42,6 @@ import android.os.SystemClock; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; -import android.text.TextUtils; import android.util.ArraySet; import android.view.View; import android.widget.ImageView; @@ -52,7 +52,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.util.ArrayUtils; import com.android.internal.util.ContrastColorUtil; -import com.android.systemui.R; import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.notification.InflationException; @@ -158,19 +157,6 @@ public final class NotificationEntry { public boolean canBubble; /** - * Whether this notification should be shown in the shade when it is also displayed as a bubble. - * - * <p>When a notification is a bubble we don't show it in the shade once the bubble has been - * expanded</p> - */ - private boolean mShowInShadeWhenBubble; - - /** - * Whether the user has dismissed this notification when it was in bubble form. - */ - private boolean mUserDismissedBubble; - - /** * Whether this notification is shown to the user as a high priority notification: visible on * the lock screen/status bar and in the top section in the shade. */ @@ -305,31 +291,6 @@ public final class NotificationEntry { return (notification.getNotification().flags & FLAG_BUBBLE) != 0; } - public void setBubbleDismissed(boolean userDismissed) { - mUserDismissedBubble = userDismissed; - } - - public boolean isBubbleDismissed() { - return mUserDismissedBubble; - } - - /** - * Sets whether this notification should be shown in the shade when it is also displayed as a - * bubble. - */ - public void setShowInShadeWhenBubble(boolean showInShade) { - mShowInShadeWhenBubble = showInShade; - } - - /** - * Whether this notification should be shown in the shade when it is also displayed as a - * bubble. - */ - public boolean showInShadeWhenBubble() { - // We always show it in the shade if non-clearable - return !isRowDismissed() && (!isClearable() || mShowInShadeWhenBubble); - } - /** * Returns the data needed for a bubble for this notification, if it exists. */ @@ -524,72 +485,6 @@ public final class NotificationEntry { } /** - * Returns our best guess for the most relevant text summary of the latest update to this - * notification, based on its type. Returns null if there should not be an update message. - */ - public CharSequence getUpdateMessage(Context context) { - final Notification underlyingNotif = notification.getNotification(); - final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle(); - - try { - if (Notification.BigTextStyle.class.equals(style)) { - // Return the big text, it is big so probably important. If it's not there use the - // normal text. - CharSequence bigText = - underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT); - return !TextUtils.isEmpty(bigText) - ? bigText - : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); - } else if (Notification.MessagingStyle.class.equals(style)) { - final List<Notification.MessagingStyle.Message> messages = - Notification.MessagingStyle.Message.getMessagesFromBundleArray( - (Parcelable[]) underlyingNotif.extras.get( - Notification.EXTRA_MESSAGES)); - - final Notification.MessagingStyle.Message latestMessage = - Notification.MessagingStyle.findLatestIncomingMessage(messages); - - if (latestMessage != null) { - final CharSequence personName = latestMessage.getSenderPerson() != null - ? latestMessage.getSenderPerson().getName() - : null; - - // Prepend the sender name if available since group chats also use messaging - // style. - if (!TextUtils.isEmpty(personName)) { - return context.getResources().getString( - R.string.notification_summary_message_format, - personName, - latestMessage.getText()); - } else { - return latestMessage.getText(); - } - } - } else if (Notification.InboxStyle.class.equals(style)) { - CharSequence[] lines = - underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES); - - // Return the last line since it should be the most recent. - if (lines != null && lines.length > 0) { - return lines[lines.length - 1]; - } - } else if (Notification.MediaStyle.class.equals(style)) { - // Return nothing, media updates aren't typically useful as a text update. - return null; - } else { - // Default to text extra. - return underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); - } - } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) { - // No use crashing, we'll just return null and the caller will assume there's no update - // message. - e.printStackTrace(); - } - - return null; - } - - /** * Abort all existing inflation tasks */ public void abortTask() { @@ -936,6 +831,16 @@ public final class NotificationEntry { return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_NOTIFICATION_LIST); } + + /** + * Returns whether {@link Policy#SUPPRESSED_EFFECT_BADGE} + * is set for this entry. This badge is not an app badge, but rather an indicator of "unseen" + * content. Typically this is referred to as a "dot" internally in Launcher & SysUI code. + */ + public boolean shouldSuppressNotificationDot() { + return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_BADGE); + } + /** * Categories that are explicitly called out on DND settings screens are always blocked, if * DND has flagged them, even if they are foreground or system notifications that might @@ -1003,12 +908,4 @@ public final class NotificationEntry { this.index = index; } } - - /** - * Returns whether the notification is a foreground service. It shows that this is an ongoing - * bubble. - */ - public boolean isForegroundService() { - return (notification.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0; - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 65e744b9e047..12d537d3c646 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -2311,9 +2311,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public int getIntrinsicHeight() { - if (isShownAsBubble()) { - return getMaxExpandHeight(); - } if (isUserLocked()) { return getActualHeight(); } @@ -2359,10 +2356,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mStatusbarStateController != null && mStatusbarStateController.isDozing(); } - private boolean isShownAsBubble() { - return mEntry.isBubble() && !mEntry.showInShadeWhenBubble() && !mEntry.isBubbleDismissed(); - } - @Override public boolean isGroupExpanded() { return mGroupManager.isGroupExpanded(mStatusBarNotification); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java index 06a2225ed0bf..ecfc45bb1182 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java @@ -604,7 +604,7 @@ public class LockIcon extends KeyguardAffordanceView implements OnUserInfoChange */ public void onScrimVisibilityChanged(@ScrimVisibility int scrimsVisible) { if (mWakeAndUnlockRunning - && scrimsVisible == ScrimController.VISIBILITY_FULLY_TRANSPARENT) { + && scrimsVisible == ScrimController.TRANSPARENT) { mWakeAndUnlockRunning = false; update(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java index 195870bde25a..adaea9379c71 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java @@ -22,6 +22,7 @@ import android.util.ArraySet; import android.util.Log; import com.android.systemui.Dependency; +import com.android.systemui.bubbles.BubbleController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; import com.android.systemui.statusbar.StatusBarState; @@ -53,12 +54,20 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, State private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>(); private HeadsUpManager mHeadsUpManager; private boolean mIsUpdatingUnchangedGroup; + @Nullable private BubbleController mBubbleController = null; @Inject public NotificationGroupManager(StatusBarStateController statusBarStateController) { statusBarStateController.addCallback(this); } + private BubbleController getBubbleController() { + if (mBubbleController == null) { + mBubbleController = Dependency.get(BubbleController.class); + } + return mBubbleController; + } + /** * Add a listener for changes to groups. * @@ -187,12 +196,22 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, State if (group == null) { return; } + int childCount = 0; + boolean hasBubbles = false; + for (String key : group.children.keySet()) { + if (!getBubbleController().isBubbleNotificationSuppressedFromShade(key)) { + childCount++; + } else { + hasBubbles = true; + } + } + boolean prevSuppressed = group.suppressed; group.suppressed = group.summary != null && !group.expanded - && (group.children.size() == 1 - || (group.children.size() == 0 + && (childCount == 1 + || (childCount == 0 && group.summary.notification.getNotification().isGroupSummary() - && hasIsolatedChildren(group))); + && (hasIsolatedChildren(group) || hasBubbles))); if (prevSuppressed != group.suppressed) { for (OnGroupChangeListener listener : mListeners) { if (!mIsUpdatingUnchangedGroup) { @@ -381,6 +400,17 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, State } /** + * If there is a {@link NotificationGroup} associated with the provided entry, this method + * will update the suppression of that group. + */ + public void updateSuppression(NotificationEntry entry) { + NotificationGroup group = mGroupMap.get(getGroupKey(entry.notification)); + if (group != null) { + updateSuppression(group); + } + } + + /** * Get the group key. May differ from the one in the notification due to the notification * being temporarily isolated. * @@ -565,6 +595,7 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, State ? Log.getStackTraceString(child.getDebugThrowable()) : ""); } + result += "\n summary suppressed: " + suppressed; return result; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index a7262cfcfefb..5733d4b0eb28 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -79,23 +79,24 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo /** * When both scrims have 0 alpha. */ - public static final int VISIBILITY_FULLY_TRANSPARENT = 0; + public static final int TRANSPARENT = 0; /** * When scrims aren't transparent (alpha 0) but also not opaque (alpha 1.) */ - public static final int VISIBILITY_SEMI_TRANSPARENT = 1; + public static final int SEMI_TRANSPARENT = 1; /** * When at least 1 scrim is fully opaque (alpha set to 1.) */ - public static final int VISIBILITY_FULLY_OPAQUE = 2; + public static final int OPAQUE = 2; - @IntDef(prefix = { "VISIBILITY_" }, value = { - VISIBILITY_FULLY_TRANSPARENT, - VISIBILITY_SEMI_TRANSPARENT, - VISIBILITY_FULLY_OPAQUE + @IntDef(prefix = {"VISIBILITY_"}, value = { + TRANSPARENT, + SEMI_TRANSPARENT, + OPAQUE }) @Retention(RetentionPolicy.SOURCE) - public @interface ScrimVisibility {} + public @interface ScrimVisibility { + } /** * Default alpha value for most scrims. @@ -123,8 +124,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo private ScrimState mState = ScrimState.UNINITIALIZED; private final Context mContext; - protected final ScrimView mScrimBehind; + protected final ScrimView mScrimInFront; + protected final ScrimView mScrimBehind; + protected final ScrimView mScrimForBubble; + private final UnlockMethodCache mUnlockMethodCache; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final DozeParameters mDozeParameters; @@ -153,10 +157,15 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo private Runnable mOnAnimationFinished; private boolean mDeferFinishedListener; private final Interpolator mInterpolator = new DecelerateInterpolator(); - private float mCurrentInFrontAlpha = NOT_INITIALIZED; - private float mCurrentBehindAlpha = NOT_INITIALIZED; - private int mCurrentInFrontTint; - private int mCurrentBehindTint; + + private float mInFrontAlpha = NOT_INITIALIZED; + private float mBehindAlpha = NOT_INITIALIZED; + private float mBubbleAlpha = NOT_INITIALIZED; + + private int mInFrontTint; + private int mBehindTint; + private int mBubbleTint; + private boolean mWallpaperVisibilityTimedOut; private int mScrimsVisibility; private final TriConsumer<ScrimState, Float, GradientColors> mScrimStateListener; @@ -175,14 +184,17 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo private boolean mWakeLockHeld; private boolean mKeyguardOccluded; - public ScrimController(ScrimView scrimBehind, ScrimView scrimInFront, + public ScrimController(ScrimView scrimBehind, ScrimView scrimInFront, ScrimView scrimForBubble, TriConsumer<ScrimState, Float, GradientColors> scrimStateListener, Consumer<Integer> scrimVisibleListener, DozeParameters dozeParameters, AlarmManager alarmManager, KeyguardMonitor keyguardMonitor) { mScrimBehind = scrimBehind; mScrimInFront = scrimInFront; + mScrimForBubble = scrimForBubble; + mScrimStateListener = scrimStateListener; mScrimVisibleListener = scrimVisibleListener; + mContext = scrimBehind.getContext(); mUnlockMethodCache = UnlockMethodCache.getInstance(mContext); mDarkenWhileDragging = !mUnlockMethodCache.canSkipBouncer(); @@ -213,12 +225,13 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo final ScrimState[] states = ScrimState.values(); for (int i = 0; i < states.length; i++) { - states[i].init(mScrimInFront, mScrimBehind, mDozeParameters); + states[i].init(mScrimInFront, mScrimBehind, mScrimForBubble, mDozeParameters); states[i].setScrimBehindAlphaKeyguard(mScrimBehindAlphaKeyguard); } mScrimBehind.setDefaultFocusHighlightEnabled(false); mScrimInFront.setDefaultFocusHighlightEnabled(false); + mScrimForBubble.setDefaultFocusHighlightEnabled(false); updateScrims(); } @@ -257,10 +270,14 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo mBlankScreen = state.getBlanksScreen(); mAnimateChange = state.getAnimateChange(); mAnimationDuration = state.getAnimationDuration(); - mCurrentInFrontTint = state.getFrontTint(); - mCurrentBehindTint = state.getBehindTint(); - mCurrentInFrontAlpha = state.getFrontAlpha(); - mCurrentBehindAlpha = state.getBehindAlpha(); + + mInFrontTint = state.getFrontTint(); + mBehindTint = state.getBehindTint(); + mBubbleTint = state.getBubbleTint(); + + mInFrontAlpha = state.getFrontAlpha(); + mBehindAlpha = state.getBehindAlpha(); + mBubbleAlpha = state.getBubbleAlpha(); applyExpansionToAlpha(); // Scrim might acquire focus when user is navigating with a D-pad or a keyboard. @@ -393,21 +410,20 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo if (mExpansionFraction != fraction) { mExpansionFraction = fraction; - final boolean keyguardOrUnlocked = mState == ScrimState.UNLOCKED - || mState == ScrimState.KEYGUARD || mState == ScrimState.PULSING; - if (!keyguardOrUnlocked || !mExpansionAffectsAlpha) { + boolean relevantState = (mState == ScrimState.UNLOCKED + || mState == ScrimState.KEYGUARD + || mState == ScrimState.PULSING + || mState == ScrimState.BUBBLE_EXPANDED); + if (!(relevantState && mExpansionAffectsAlpha)) { return; } - applyExpansionToAlpha(); - if (mUpdatePending) { return; } - setOrAdaptCurrentAnimation(mScrimBehind); setOrAdaptCurrentAnimation(mScrimInFront); - + setOrAdaptCurrentAnimation(mScrimForBubble); dispatchScrimState(mScrimBehind.getViewAlpha()); // Reset wallpaper timeout if it's already timeout like expanding panel while PULSING @@ -421,11 +437,10 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo } private void setOrAdaptCurrentAnimation(View scrim) { - if (!isAnimating(scrim)) { - updateScrimColor(scrim, getCurrentScrimAlpha(scrim), getCurrentScrimTint(scrim)); - } else { + float alpha = getCurrentScrimAlpha(scrim); + if (isAnimating(scrim)) { + // Adapt current animation. ValueAnimator previousAnimator = (ValueAnimator) scrim.getTag(TAG_KEY_ANIM); - float alpha = getCurrentScrimAlpha(scrim); float previousEndValue = (Float) scrim.getTag(TAG_END_ALPHA); float previousStartValue = (Float) scrim.getTag(TAG_START_ALPHA); float relativeDiff = alpha - previousEndValue; @@ -433,6 +448,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo scrim.setTag(TAG_START_ALPHA, newStartValue); scrim.setTag(TAG_END_ALPHA, alpha); previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); + } else { + // Set animation. + updateScrimColor(scrim, alpha, getCurrentScrimTint(scrim)); } } @@ -441,27 +459,27 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo return; } - if (mState == ScrimState.UNLOCKED) { + if (mState == ScrimState.UNLOCKED || mState == ScrimState.BUBBLE_EXPANDED) { // Darken scrim as you pull down the shade when unlocked float behindFraction = getInterpolatedFraction(); behindFraction = (float) Math.pow(behindFraction, 0.8f); - mCurrentBehindAlpha = behindFraction * GRADIENT_SCRIM_ALPHA_BUSY; - mCurrentInFrontAlpha = 0; + mBehindAlpha = behindFraction * GRADIENT_SCRIM_ALPHA_BUSY; + mInFrontAlpha = 0; } else if (mState == ScrimState.KEYGUARD || mState == ScrimState.PULSING) { // Either darken of make the scrim transparent when you // pull down the shade float interpolatedFract = getInterpolatedFraction(); float alphaBehind = mState.getBehindAlpha(); if (mDarkenWhileDragging) { - mCurrentBehindAlpha = MathUtils.lerp(GRADIENT_SCRIM_ALPHA_BUSY, alphaBehind, + mBehindAlpha = MathUtils.lerp(GRADIENT_SCRIM_ALPHA_BUSY, alphaBehind, interpolatedFract); - mCurrentInFrontAlpha = 0; + mInFrontAlpha = 0; } else { - mCurrentBehindAlpha = MathUtils.lerp(0 /* start */, alphaBehind, + mBehindAlpha = MathUtils.lerp(0 /* start */, alphaBehind, interpolatedFract); - mCurrentInFrontAlpha = 0; + mInFrontAlpha = 0; } - mCurrentBehindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getBehindTint(), + mBehindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getBehindTint(), mState.getBehindTint(), interpolatedFract); } } @@ -486,8 +504,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo */ public void setAodFrontScrimAlpha(float alpha) { if (mState == ScrimState.AOD && mDozeParameters.getAlwaysOn() - && mCurrentInFrontAlpha != alpha) { - mCurrentInFrontAlpha = alpha; + && mInFrontAlpha != alpha) { + mInFrontAlpha = alpha; updateScrims(); } @@ -499,10 +517,10 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo * away once the display turns on. */ public void prepareForGentleWakeUp() { - if (mState == ScrimState.AOD) { - mCurrentInFrontAlpha = 1f; - mCurrentInFrontTint = Color.BLACK; - mCurrentBehindTint = Color.BLACK; + if (mState == ScrimState.AOD && mDozeParameters.getAlwaysOn()) { + mInFrontAlpha = 1f; + mInFrontTint = Color.BLACK; + mBehindTint = Color.BLACK; mAnimateChange = false; updateScrims(); mAnimateChange = true; @@ -520,8 +538,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo if (mState == ScrimState.PULSING) { float newBehindAlpha = mState.getBehindAlpha(); - if (mCurrentBehindAlpha != newBehindAlpha) { - mCurrentBehindAlpha = newBehindAlpha; + if (mBehindAlpha != newBehindAlpha) { + mBehindAlpha = newBehindAlpha; updateScrims(); } } @@ -543,8 +561,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo // Only animate scrim color if the scrim view is actually visible boolean animateScrimInFront = mScrimInFront.getViewAlpha() != 0 && !mBlankScreen; boolean animateScrimBehind = mScrimBehind.getViewAlpha() != 0 && !mBlankScreen; + boolean animateScrimForBubble = mScrimForBubble.getViewAlpha() != 0 && !mBlankScreen; + mScrimInFront.setColors(mColors, animateScrimInFront); mScrimBehind.setColors(mColors, animateScrimBehind); + mScrimForBubble.setColors(mColors, animateScrimForBubble); // Calculate minimum scrim opacity for white or black text. int textColor = mColors.supportsDarkText() ? Color.BLACK : Color.WHITE; @@ -563,12 +584,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo boolean occludedKeyguard = (mState == ScrimState.PULSING || mState == ScrimState.AOD) && mKeyguardOccluded; if (aodWallpaperTimeout || occludedKeyguard) { - mCurrentBehindAlpha = 1; + mBehindAlpha = 1; } - - setScrimInFrontAlpha(mCurrentInFrontAlpha); - setScrimBehindAlpha(mCurrentBehindAlpha); - + setScrimAlpha(mScrimInFront, mInFrontAlpha); + setScrimAlpha(mScrimBehind, mBehindAlpha); + setScrimAlpha(mScrimForBubble, mBubbleAlpha); dispatchScrimsVisible(); } @@ -579,11 +599,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo private void dispatchScrimsVisible() { final int currentScrimVisibility; if (mScrimInFront.getViewAlpha() == 1 || mScrimBehind.getViewAlpha() == 1) { - currentScrimVisibility = VISIBILITY_FULLY_OPAQUE; + currentScrimVisibility = OPAQUE; } else if (mScrimInFront.getViewAlpha() == 0 && mScrimBehind.getViewAlpha() == 0) { - currentScrimVisibility = VISIBILITY_FULLY_TRANSPARENT; + currentScrimVisibility = TRANSPARENT; } else { - currentScrimVisibility = VISIBILITY_SEMI_TRANSPARENT; + currentScrimVisibility = SEMI_TRANSPARENT; } if (mScrimsVisibility != currentScrimVisibility) { @@ -600,18 +620,10 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo return 0; } else { // woo, special effects - return (float)(1f-0.5f*(1f-Math.cos(3.14159f * Math.pow(1f-frac, 2f)))); + return (float) (1f - 0.5f * (1f - Math.cos(3.14159f * Math.pow(1f - frac, 2f)))); } } - private void setScrimBehindAlpha(float alpha) { - setScrimAlpha(mScrimBehind, alpha); - } - - private void setScrimInFrontAlpha(float alpha) { - setScrimAlpha(mScrimInFront, alpha); - } - private void setScrimAlpha(ScrimView scrim, float alpha) { if (alpha == 0f) { scrim.setClickable(false); @@ -622,17 +634,26 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo updateScrim(scrim, alpha); } + private String getScrimName(ScrimView scrim) { + if (scrim == mScrimInFront) { + return "front_scrim"; + } else if (scrim == mScrimBehind) { + return "back_scrim"; + } else if (scrim == mScrimForBubble) { + return "bubble_scrim"; + } + return "unknown_scrim"; + } + private void updateScrimColor(View scrim, float alpha, int tint) { alpha = Math.max(0, Math.min(1.0f, alpha)); if (scrim instanceof ScrimView) { ScrimView scrimView = (ScrimView) scrim; - Trace.traceCounter(Trace.TRACE_TAG_APP, - scrim == mScrimInFront ? "front_scrim_alpha" : "back_scrim_alpha", + Trace.traceCounter(Trace.TRACE_TAG_APP, getScrimName(scrimView) + "_alpha", (int) (alpha * 255)); - Trace.traceCounter(Trace.TRACE_TAG_APP, - scrim == mScrimInFront ? "front_scrim_tint" : "back_scrim_tint", + Trace.traceCounter(Trace.TRACE_TAG_APP, getScrimName(scrimView) + "_tint", Color.alpha(tint)); scrimView.setTint(tint); @@ -689,9 +710,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo private float getCurrentScrimAlpha(View scrim) { if (scrim == mScrimInFront) { - return mCurrentInFrontAlpha; + return mInFrontAlpha; } else if (scrim == mScrimBehind) { - return mCurrentBehindAlpha; + return mBehindAlpha; + } else if (scrim == mScrimForBubble) { + return mBubbleAlpha; } else { throw new IllegalArgumentException("Unknown scrim view"); } @@ -699,9 +722,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo private int getCurrentScrimTint(View scrim) { if (scrim == mScrimInFront) { - return mCurrentInFrontTint; + return mInFrontTint; } else if (scrim == mScrimBehind) { - return mCurrentBehindTint; + return mBehindTint; + } else if (scrim == mScrimForBubble) { + return mBubbleTint; } else { throw new IllegalArgumentException("Unknown scrim view"); } @@ -744,8 +769,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo // When unlocking with fingerprint, we'll fade the scrims from black to transparent. // At the end of the animation we need to remove the tint. if (mState == ScrimState.UNLOCKED) { - mCurrentInFrontTint = Color.TRANSPARENT; - mCurrentBehindTint = Color.TRANSPARENT; + mInFrontTint = Color.TRANSPARENT; + mBehindTint = Color.TRANSPARENT; + mBubbleTint = Color.TRANSPARENT; } } @@ -850,6 +876,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo /** * Executes a callback after the frame has hit the display. + * * @param callback What to run. */ @VisibleForTesting @@ -893,16 +920,35 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println(" ScrimController: "); - pw.print(" state: "); pw.println(mState); - pw.print(" frontScrim:"); pw.print(" viewAlpha="); pw.print(mScrimInFront.getViewAlpha()); - pw.print(" alpha="); pw.print(mCurrentInFrontAlpha); - pw.print(" tint=0x"); pw.println(Integer.toHexString(mScrimInFront.getTint())); - - pw.print(" backScrim:"); pw.print(" viewAlpha="); pw.print(mScrimBehind.getViewAlpha()); - pw.print(" alpha="); pw.print(mCurrentBehindAlpha); - pw.print(" tint=0x"); pw.println(Integer.toHexString(mScrimBehind.getTint())); - - pw.print(" mTracking="); pw.println(mTracking); + pw.print(" state: "); + pw.println(mState); + + pw.print(" frontScrim:"); + pw.print(" viewAlpha="); + pw.print(mScrimInFront.getViewAlpha()); + pw.print(" alpha="); + pw.print(mInFrontAlpha); + pw.print(" tint=0x"); + pw.println(Integer.toHexString(mScrimInFront.getTint())); + + pw.print(" backScrim:"); + pw.print(" viewAlpha="); + pw.print(mScrimBehind.getViewAlpha()); + pw.print(" alpha="); + pw.print(mBehindAlpha); + pw.print(" tint=0x"); + pw.println(Integer.toHexString(mScrimBehind.getTint())); + + pw.print(" bubbleScrim:"); + pw.print(" viewAlpha="); + pw.print(mScrimForBubble.getViewAlpha()); + pw.print(" alpha="); + pw.print(mBubbleAlpha); + pw.print(" tint=0x"); + pw.println(Integer.toHexString(mScrimForBubble.getTint())); + + pw.print(" mTracking="); + pw.println(mTracking); } public void setWallpaperSupportsAmbientMode(boolean wallpaperSupportsAmbientMode) { @@ -949,8 +995,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo // in this case, back-scrim needs to be re-evaluated if (mState == ScrimState.AOD || mState == ScrimState.PULSING) { float newBehindAlpha = mState.getBehindAlpha(); - if (mCurrentBehindAlpha != newBehindAlpha) { - mCurrentBehindAlpha = newBehindAlpha; + if (mBehindAlpha != newBehindAlpha) { + mBehindAlpha = newBehindAlpha; updateScrims(); } } @@ -971,10 +1017,13 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo public interface Callback { default void onStart() { } + default void onDisplayBlanked() { } + default void onFinished() { } + default void onCancelled() { } /** Returns whether to timeout wallpaper or not. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index 9fdd3b88e9d0..c9acbad1e8cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -36,7 +36,6 @@ public enum ScrimState { * On the lock screen. */ KEYGUARD(0) { - @Override public void prepare(ScrimState previousState) { mBlankScreen = false; @@ -53,10 +52,13 @@ public enum ScrimState { } else { mAnimationDuration = ScrimController.ANIMATION_DURATION; } - mCurrentInFrontTint = Color.BLACK; - mCurrentBehindTint = Color.BLACK; - mCurrentBehindAlpha = mScrimBehindAlphaKeyguard; - mCurrentInFrontAlpha = 0; + mFrontTint = Color.BLACK; + mBehindTint = Color.BLACK; + mBubbleTint = Color.TRANSPARENT; + + mFrontAlpha = 0; + mBehindAlpha = mScrimBehindAlphaKeyguard; + mBubbleAlpha = 0; } }, @@ -66,8 +68,9 @@ public enum ScrimState { BOUNCER(1) { @Override public void prepare(ScrimState previousState) { - mCurrentBehindAlpha = ScrimController.GRADIENT_SCRIM_ALPHA_BUSY; - mCurrentInFrontAlpha = 0f; + mBehindAlpha = ScrimController.GRADIENT_SCRIM_ALPHA_BUSY; + mFrontAlpha = 0f; + mBubbleAlpha = 0f; } }, @@ -77,8 +80,9 @@ public enum ScrimState { BOUNCER_SCRIMMED(2) { @Override public void prepare(ScrimState previousState) { - mCurrentBehindAlpha = 0; - mCurrentInFrontAlpha = ScrimController.GRADIENT_SCRIM_ALPHA_BUSY; + mBehindAlpha = 0; + mBubbleAlpha = 0f; + mFrontAlpha = ScrimController.GRADIENT_SCRIM_ALPHA_BUSY; } }, @@ -88,8 +92,9 @@ public enum ScrimState { BRIGHTNESS_MIRROR(3) { @Override public void prepare(ScrimState previousState) { - mCurrentBehindAlpha = 0; - mCurrentInFrontAlpha = 0; + mBehindAlpha = 0; + mFrontAlpha = 0; + mBubbleAlpha = 0; } }, @@ -101,9 +106,16 @@ public enum ScrimState { public void prepare(ScrimState previousState) { final boolean alwaysOnEnabled = mDozeParameters.getAlwaysOn(); mBlankScreen = mDisplayRequiresBlanking; - mCurrentInFrontAlpha = alwaysOnEnabled ? mAodFrontScrimAlpha : 1f; - mCurrentInFrontTint = Color.BLACK; - mCurrentBehindTint = Color.BLACK; + + mFrontTint = Color.BLACK; + mFrontAlpha = alwaysOnEnabled ? mAodFrontScrimAlpha : 1f; + + mBehindTint = Color.BLACK; + mBehindAlpha = ScrimController.TRANSPARENT; + + mBubbleTint = Color.TRANSPARENT; + mBubbleAlpha = ScrimController.TRANSPARENT; + mAnimationDuration = ScrimController.ANIMATION_DURATION_LONG; // DisplayPowerManager may blank the screen for us, // in this case we just need to set our state. @@ -127,9 +139,10 @@ public enum ScrimState { PULSING(5) { @Override public void prepare(ScrimState previousState) { - mCurrentInFrontAlpha = 0f; - mCurrentBehindTint = Color.BLACK; - mCurrentInFrontTint = Color.BLACK; + mFrontAlpha = 0f; + mBubbleAlpha = 0f; + mBehindTint = Color.BLACK; + mFrontTint = Color.BLACK; mBlankScreen = mDisplayRequiresBlanking; mAnimationDuration = mWakeLockScreenSensorActive ? ScrimController.ANIMATION_DURATION_LONG : ScrimController.ANIMATION_DURATION; @@ -154,25 +167,33 @@ public enum ScrimState { UNLOCKED(6) { @Override public void prepare(ScrimState previousState) { - mCurrentBehindAlpha = 0; - mCurrentInFrontAlpha = 0; + // State that UI will sync to. + mBehindAlpha = 0; + mFrontAlpha = 0; + mBubbleAlpha = 0; + mAnimationDuration = mKeyguardFadingAway ? mKeyguardFadingAwayDuration : StatusBar.FADE_KEYGUARD_DURATION; + mAnimateChange = !mLaunchingAffordanceWithPreview; + mFrontTint = Color.TRANSPARENT; + mBehindTint = Color.TRANSPARENT; + mBubbleTint = Color.TRANSPARENT; + mBlankScreen = false; + if (previousState == ScrimState.AOD) { - // Fade from black to transparent when coming directly from AOD - updateScrimColor(mScrimInFront, 1, Color.BLACK); - updateScrimColor(mScrimBehind, 1, Color.BLACK); + // Set all scrims black, before they fade transparent. + updateScrimColor(mScrimInFront, 1f /* alpha */, Color.BLACK /* tint */); + updateScrimColor(mScrimBehind, 1f /* alpha */, Color.BLACK /* tint */); + updateScrimColor(mScrimForBubble, 1f /* alpha */, Color.BLACK /* tint */); + // Scrims should still be black at the end of the transition. - mCurrentInFrontTint = Color.BLACK; - mCurrentBehindTint = Color.BLACK; + mFrontTint = Color.BLACK; + mBehindTint = Color.BLACK; + mBubbleTint = Color.BLACK; mBlankScreen = true; - } else { - mCurrentInFrontTint = Color.TRANSPARENT; - mCurrentBehindTint = Color.TRANSPARENT; - mBlankScreen = false; } } }, @@ -183,25 +204,36 @@ public enum ScrimState { BUBBLE_EXPANDED(7) { @Override public void prepare(ScrimState previousState) { - mCurrentInFrontTint = Color.TRANSPARENT; - mCurrentBehindTint = Color.TRANSPARENT; + mFrontTint = Color.TRANSPARENT; + mBehindTint = Color.TRANSPARENT; + mBubbleTint = Color.TRANSPARENT; + + mFrontAlpha = ScrimController.TRANSPARENT; + mBehindAlpha = ScrimController.GRADIENT_SCRIM_ALPHA_BUSY; + mBubbleAlpha = ScrimController.GRADIENT_SCRIM_ALPHA_BUSY; + mAnimationDuration = ScrimController.ANIMATION_DURATION; - mCurrentBehindAlpha = ScrimController.GRADIENT_SCRIM_ALPHA_BUSY; mBlankScreen = false; } }; boolean mBlankScreen = false; long mAnimationDuration = ScrimController.ANIMATION_DURATION; - int mCurrentInFrontTint = Color.TRANSPARENT; - int mCurrentBehindTint = Color.TRANSPARENT; + int mFrontTint = Color.TRANSPARENT; + int mBehindTint = Color.TRANSPARENT; + int mBubbleTint = Color.TRANSPARENT; + boolean mAnimateChange = true; - float mCurrentInFrontAlpha; - float mCurrentBehindAlpha; float mAodFrontScrimAlpha; + float mFrontAlpha; + float mBehindAlpha; + float mBubbleAlpha; + float mScrimBehindAlphaKeyguard; ScrimView mScrimInFront; ScrimView mScrimBehind; + ScrimView mScrimForBubble; + DozeParameters mDozeParameters; boolean mDisplayRequiresBlanking; boolean mWallpaperSupportsAmbientMode; @@ -216,13 +248,17 @@ public enum ScrimState { mIndex = index; } - public void init(ScrimView scrimInFront, ScrimView scrimBehind, DozeParameters dozeParameters) { + public void init(ScrimView scrimInFront, ScrimView scrimBehind, ScrimView scrimForBubble, + DozeParameters dozeParameters) { mScrimInFront = scrimInFront; mScrimBehind = scrimBehind; + mScrimForBubble = scrimForBubble; + mDozeParameters = dozeParameters; mDisplayRequiresBlanking = dozeParameters.getDisplayNeedsBlanking(); } + /** Prepare state for transition. */ public void prepare(ScrimState previousState) { } @@ -231,19 +267,27 @@ public enum ScrimState { } public float getFrontAlpha() { - return mCurrentInFrontAlpha; + return mFrontAlpha; } public float getBehindAlpha() { - return mCurrentBehindAlpha; + return mBehindAlpha; + } + + public float getBubbleAlpha() { + return mBubbleAlpha; } public int getFrontTint() { - return mCurrentInFrontTint; + return mFrontTint; } public int getBehindTint() { - return mCurrentBehindTint; + return mBehindTint; + } + + public int getBubbleTint() { + return mBubbleTint; } public long getAnimationDuration() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 9fc3e476579a..1c857240114c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -824,6 +824,7 @@ public class StatusBar extends SystemUI implements DemoMode, // TODO: Deal with the ugliness that comes from having some of the statusbar broken out // into fragments, but the rest here, it leaves some awkward lifecycle and whatnot. mNotificationPanel = mStatusBarWindow.findViewById(R.id.notification_panel); + mStackScroller = mStatusBarWindow.findViewById(R.id.notification_stack_scroller); mZenController.addCallback(this); NotificationListContainer notifListContainer = (NotificationListContainer) mStackScroller; @@ -944,8 +945,10 @@ public class StatusBar extends SystemUI implements DemoMode, ScrimView scrimBehind = mStatusBarWindow.findViewById(R.id.scrim_behind); ScrimView scrimInFront = mStatusBarWindow.findViewById(R.id.scrim_in_front); + ScrimView scrimForBubble = mStatusBarWindow.findViewById(R.id.scrim_for_bubble); + mScrimController = SystemUIFactory.getInstance().createScrimController( - scrimBehind, scrimInFront, mLockscreenWallpaper, + scrimBehind, scrimInFront, scrimForBubble, mLockscreenWallpaper, (state, alpha, color) -> mLightBarController.setScrimState(state, alpha, color), scrimsVisible -> { if (mStatusBarWindowController != null) { @@ -2417,6 +2420,10 @@ public class StatusBar extends SystemUI implements DemoMode, pw.println(" mGroupManager: null"); } + if (mBubbleController != null) { + mBubbleController.dump(fd, pw, args); + } + if (mLightBarController != null) { mLightBarController.dump(fd, pw, args); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java index e85b147f7a34..58519b8e8a58 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java @@ -223,7 +223,7 @@ public class StatusBarWindowController implements Callback, Dumpable, Configurat } final boolean scrimsOccludingWallpaper = - state.scrimsVisibility == ScrimController.VISIBILITY_FULLY_OPAQUE; + state.scrimsVisibility == ScrimController.OPAQUE; final boolean keyguardOrAod = state.keyguardShowing || (state.dozing && mDozeParameters.getAlwaysOn()); if (keyguardOrAod && !state.backdropShowing && !scrimsOccludingWallpaper) { @@ -309,7 +309,7 @@ public class StatusBarWindowController implements Callback, Dumpable, Configurat return !state.forceCollapsed && (state.isKeyguardShowingAndNotOccluded() || state.panelVisible || state.keyguardFadingAway || state.bouncerShowing || state.headsUpShowing || state.bubblesShowing - || state.scrimsVisibility != ScrimController.VISIBILITY_FULLY_TRANSPARENT); + || state.scrimsVisibility != ScrimController.TRANSPARENT); } private void applyFitsSystemWindows(State state) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java index 2221915a627a..ba434d4fd0bd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java @@ -17,7 +17,6 @@ package com.android.systemui.bubbles; import static android.app.Notification.FLAG_BUBBLE; -import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL; @@ -28,6 +27,8 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -42,11 +43,10 @@ import static org.mockito.Mockito.when; import android.app.IActivityManager; import android.app.Notification; import android.app.PendingIntent; -import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.graphics.drawable.Icon; +import android.hardware.face.FaceManager; import android.service.notification.ZenModeConfig; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -57,17 +57,21 @@ import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoveInterceptor; import com.android.systemui.statusbar.NotificationTestHelper; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.NotificationFilter; import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; import com.android.systemui.statusbar.notification.collection.NotificationData; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.phone.DozeParameters; +import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.StatusBarWindowController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -82,20 +86,16 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) public class BubbleControllerTest extends SysuiTestCase { - // Some APIs rely on the app being foreground, check is via pkg name - private static final String FOREGROUND_TEST_PKG_NAME = "com.android.systemui.tests"; - @Mock private NotificationEntryManager mNotificationEntryManager; @Mock + private NotificationGroupManager mNotificationGroupManager; + @Mock private WindowManager mWindowManager; @Mock private IActivityManager mActivityManager; @@ -108,6 +108,10 @@ public class BubbleControllerTest extends SysuiTestCase { @Mock private ZenModeConfig mZenModeConfig; @Mock + private FaceManager mFaceManager; + @Mock + private NotificationLockscreenUserManager mLockscreenUserManager; + @Mock private SysuiStatusBarStateController mStatusBarStateController; @Mock private KeyguardBypassController mKeyguardBypassController; @@ -126,8 +130,6 @@ public class BubbleControllerTest extends SysuiTestCase { private NotificationTestHelper mNotificationTestHelper; private ExpandableNotificationRow mRow; private ExpandableNotificationRow mRow2; - private ExpandableNotificationRow mAutoExpandRow; - private ExpandableNotificationRow mSuppressNotifRow; private ExpandableNotificationRow mNonBubbleNotifRow; @Mock @@ -146,6 +148,7 @@ public class BubbleControllerTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mStatusBarView = new FrameLayout(mContext); mDependency.injectTestDependency(NotificationEntryManager.class, mNotificationEntryManager); + mContext.addMockSystemService(FaceManager.class, mFaceManager); // Bubbles get added to status bar window view mStatusBarWindowController = new StatusBarWindowController(mContext, mWindowManager, @@ -159,35 +162,31 @@ public class BubbleControllerTest extends SysuiTestCase { mRow2 = mNotificationTestHelper.createBubble(mDeleteIntent); mNonBubbleNotifRow = mNotificationTestHelper.createRow(); - // Some bubbles want to auto expand - Notification.BubbleMetadata autoExpandMetadata = - getBuilder().setAutoExpandBubble(true).build(); - mAutoExpandRow = mNotificationTestHelper.createBubble(autoExpandMetadata, - FOREGROUND_TEST_PKG_NAME); - - // Some bubbles want to suppress notifs - Notification.BubbleMetadata suppressNotifMetadata = - getBuilder().setSuppressNotification(true).build(); - mSuppressNotifRow = mNotificationTestHelper.createBubble(suppressNotifMetadata, - FOREGROUND_TEST_PKG_NAME); - // Return non-null notification data from the NEM when(mNotificationEntryManager.getNotificationData()).thenReturn(mNotificationData); + when(mNotificationData.get(mRow.getEntry().key)).thenReturn(mRow.getEntry()); when(mNotificationData.getChannel(mRow.getEntry().key)).thenReturn(mRow.getEntry().channel); mZenModeConfig.suppressedVisualEffects = 0; when(mZenModeController.getConfig()).thenReturn(mZenModeConfig); TestableNotificationInterruptionStateProvider interruptionStateProvider = - new TestableNotificationInterruptionStateProvider(mContext); + new TestableNotificationInterruptionStateProvider(mContext, + mock(NotificationFilter.class), + mock(StatusBarStateController.class)); interruptionStateProvider.setUpWithPresenter( mock(NotificationPresenter.class), mock(HeadsUpManager.class), mock(NotificationInterruptionStateProvider.HeadsUpSuppressor.class)); mBubbleData = new BubbleData(mContext); - mBubbleController = new TestableBubbleController(mContext, mStatusBarWindowController, - mBubbleData, mConfigurationController, interruptionStateProvider, - mZenModeController); + mBubbleController = new TestableBubbleController(mContext, + mStatusBarWindowController, + mBubbleData, + mConfigurationController, + interruptionStateProvider, + mZenModeController, + mLockscreenUserManager, + mNotificationGroupManager); mBubbleController.setBubbleStateChangeListener(mBubbleStateChangeListener); mBubbleController.setExpandListener(mBubbleExpandListener); @@ -219,13 +218,14 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void testRemoveBubble() { mBubbleController.updateBubble(mRow.getEntry()); + assertNotNull(mBubbleData.getBubbleWithKey(mRow.getEntry().key)); assertTrue(mBubbleController.hasBubbles()); verify(mNotificationEntryManager).updateNotifications(); verify(mBubbleStateChangeListener).onHasBubblesChanged(true); mBubbleController.removeBubble(mRow.getEntry().key, BubbleController.DISMISS_USER_GESTURE); assertFalse(mStatusBarWindowController.getBubblesShowing()); - assertTrue(mRow.getEntry().isBubbleDismissed()); + assertNull(mBubbleData.getBubbleWithKey(mRow.getEntry().key)); verify(mNotificationEntryManager, times(2)).updateNotifications(); verify(mBubbleStateChangeListener).onHasBubblesChanged(false); } @@ -236,10 +236,10 @@ public class BubbleControllerTest extends SysuiTestCase { mBubbleController.updateBubble(mRow.getEntry()); assertTrue(mBubbleController.hasBubbles()); - assertTrue(mRow.getEntry().showInShadeWhenBubble()); + assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); // Make it look like dismissed notif - mRow.getEntry().setShowInShadeWhenBubble(false); + mBubbleData.getBubbleWithKey(mRow.getEntry().key).setShowInShadeWhenBubble(false); // Now remove the bubble mBubbleController.removeBubble(mRow.getEntry().key, BubbleController.DISMISS_USER_GESTURE); @@ -255,15 +255,17 @@ public class BubbleControllerTest extends SysuiTestCase { public void testDismissStack() { mBubbleController.updateBubble(mRow.getEntry()); verify(mNotificationEntryManager, times(1)).updateNotifications(); + assertNotNull(mBubbleData.getBubbleWithKey(mRow.getEntry().key)); mBubbleController.updateBubble(mRow2.getEntry()); verify(mNotificationEntryManager, times(2)).updateNotifications(); + assertNotNull(mBubbleData.getBubbleWithKey(mRow2.getEntry().key)); assertTrue(mBubbleController.hasBubbles()); mBubbleController.dismissStack(BubbleController.DISMISS_USER_GESTURE); assertFalse(mStatusBarWindowController.getBubblesShowing()); verify(mNotificationEntryManager, times(3)).updateNotifications(); - assertTrue(mRow.getEntry().isBubbleDismissed()); - assertTrue(mRow2.getEntry().isBubbleDismissed()); + assertNull(mBubbleData.getBubbleWithKey(mRow.getEntry().key)); + assertNull(mBubbleData.getBubbleWithKey(mRow2.getEntry().key)); } @Test @@ -274,9 +276,9 @@ public class BubbleControllerTest extends SysuiTestCase { mEntryListener.onPendingEntryAdded(mRow.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); - // We should have bubbles & their notifs should show in the shade + // We should have bubbles & their notifs should not be suppressed assertTrue(mBubbleController.hasBubbles()); - assertTrue(mRow.getEntry().showInShadeWhenBubble()); + assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); assertFalse(mStatusBarWindowController.getBubbleExpanded()); // Expand the stack @@ -286,8 +288,8 @@ public class BubbleControllerTest extends SysuiTestCase { verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().key); assertTrue(mStatusBarWindowController.getBubbleExpanded()); - // Make sure it's no longer in the shade - assertFalse(mRow.getEntry().showInShadeWhenBubble()); + // Make sure the notif is suppressed + assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); // Collapse mBubbleController.collapseStack(); @@ -304,10 +306,11 @@ public class BubbleControllerTest extends SysuiTestCase { mBubbleController.updateBubble(mRow.getEntry()); mBubbleController.updateBubble(mRow2.getEntry()); - // We should have bubbles & their notifs should show in the shade + // We should have bubbles & their notifs should not be suppressed assertTrue(mBubbleController.hasBubbles()); - assertTrue(mRow.getEntry().showInShadeWhenBubble()); - assertTrue(mRow2.getEntry().showInShadeWhenBubble()); + assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); + assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade( + mRow2.getEntry().key)); // Expand BubbleStackView stackView = mBubbleController.getStackView(); @@ -317,13 +320,13 @@ public class BubbleControllerTest extends SysuiTestCase { // Last added is the one that is expanded assertEquals(mRow2.getEntry(), stackView.getExpandedBubbleView().getEntry()); - assertFalse(mRow2.getEntry().showInShadeWhenBubble()); + assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow2.getEntry().key)); // Switch which bubble is expanded mBubbleController.selectBubble(mRow.getEntry().key); - stackView.setExpandedBubble(mRow.getEntry()); + stackView.setExpandedBubble(mRow.getEntry().key); assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry()); - assertFalse(mRow.getEntry().showInShadeWhenBubble()); + assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); // collapse for previous bubble verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().key); @@ -336,14 +339,37 @@ public class BubbleControllerTest extends SysuiTestCase { } @Test - public void testExpansionRemovesShowInShade() { + public void testExpansionRemovesShowInShadeAndDot() { + // Mark it as a bubble and add it explicitly + mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mBubbleController.updateBubble(mRow.getEntry()); + + // We should have bubbles & their notifs should not be suppressed + assertTrue(mBubbleController.hasBubbles()); + assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); + assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().key).showBubbleDot()); + + // Expand + mBubbleController.expandStack(); + assertTrue(mBubbleController.isStackExpanded()); + verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().key); + + // Notif is suppressed after expansion + assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); + // Notif shouldn't show dot after expansion + assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().key).showBubbleDot()); + } + + @Test + public void testUpdateWhileExpanded_DoesntChangeShowInShadeAndDot() { // Mark it as a bubble and add it explicitly mEntryListener.onPendingEntryAdded(mRow.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); - // We should have bubbles & their notifs should show in the shade + // We should have bubbles & their notifs should not be suppressed assertTrue(mBubbleController.hasBubbles()); - assertTrue(mRow.getEntry().showInShadeWhenBubble()); + assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); + assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().key).showBubbleDot()); // Expand BubbleStackView stackView = mBubbleController.getStackView(); @@ -351,8 +377,19 @@ public class BubbleControllerTest extends SysuiTestCase { assertTrue(mBubbleController.isStackExpanded()); verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().key); - // No longer show shade in notif after expansion - assertFalse(mRow.getEntry().showInShadeWhenBubble()); + // Notif is suppressed after expansion + assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); + // Notif shouldn't show dot after expansion + assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().key).showBubbleDot()); + + // Send update + mEntryListener.onPreEntryUpdated(mRow.getEntry()); + + // Nothing should have changed + // Notif is suppressed after expansion + assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); + // Notif shouldn't show dot after expansion + assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().key).showBubbleDot()); } @Test @@ -373,7 +410,7 @@ public class BubbleControllerTest extends SysuiTestCase { // Last added is the one that is expanded assertEquals(mRow2.getEntry(), stackView.getExpandedBubbleView().getEntry()); - assertFalse(mRow2.getEntry().showInShadeWhenBubble()); + assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow2.getEntry().key)); // Dismiss currently expanded mBubbleController.removeBubble(stackView.getExpandedBubbleView().getKey(), @@ -397,14 +434,16 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void testAutoExpand_FailsNotForeground() { assertFalse(mBubbleController.isStackExpanded()); + setMetadataFlags(mRow.getEntry(), + Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE, false /* enableFlag */); // Add the auto expand bubble - mEntryListener.onPendingEntryAdded(mAutoExpandRow.getEntry()); - mBubbleController.updateBubble(mAutoExpandRow.getEntry()); + mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mBubbleController.updateBubble(mRow.getEntry()); // Expansion shouldn't change verify(mBubbleExpandListener, never()).onBubbleExpandChanged(false /* expanded */, - mAutoExpandRow.getEntry().key); + mRow.getEntry().key); assertFalse(mBubbleController.isStackExpanded()); // # of bubbles should change @@ -413,91 +452,51 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void testAutoExpand_SucceedsForeground() { - final CountDownLatch latch = new CountDownLatch(1); - BroadcastReceiver receiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - latch.countDown(); - } - }; - IntentFilter filter = new IntentFilter(BubblesTestActivity.BUBBLE_ACTIVITY_OPENED); - mContext.registerReceiver(receiver, filter); - - assertFalse(mBubbleController.isStackExpanded()); - - // Make ourselves foreground - Intent i = new Intent(mContext, BubblesTestActivity.class); - i.setFlags(FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(i); - - try { - latch.await(100, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } + setMetadataFlags(mRow.getEntry(), + Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE, true /* enableFlag */); // Add the auto expand bubble - mEntryListener.onPendingEntryAdded(mAutoExpandRow.getEntry()); - mBubbleController.updateBubble(mAutoExpandRow.getEntry()); + mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mBubbleController.updateBubble(mRow.getEntry()); // Expansion should change verify(mBubbleExpandListener).onBubbleExpandChanged(true /* expanded */, - mAutoExpandRow.getEntry().key); + mRow.getEntry().key); assertTrue(mBubbleController.isStackExpanded()); // # of bubbles should change verify(mBubbleStateChangeListener).onHasBubblesChanged(true /* hasBubbles */); - mContext.unregisterReceiver(receiver); } @Test public void testSuppressNotif_FailsNotForeground() { - // Add the suppress notif bubble - mEntryListener.onPendingEntryAdded(mSuppressNotifRow.getEntry()); - mBubbleController.updateBubble(mSuppressNotifRow.getEntry()); + setMetadataFlags(mRow.getEntry(), + Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, false /* enableFlag */); - // Should show in shade because we weren't forground - assertTrue(mSuppressNotifRow.getEntry().showInShadeWhenBubble()); + // Add the suppress notif bubble + mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mBubbleController.updateBubble(mRow.getEntry()); + // Should not be suppressed because we weren't forground + assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); // # of bubbles should change verify(mBubbleStateChangeListener).onHasBubblesChanged(true /* hasBubbles */); } @Test public void testSuppressNotif_SucceedsForeground() { - final CountDownLatch latch = new CountDownLatch(1); - BroadcastReceiver receiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - latch.countDown(); - } - }; - IntentFilter filter = new IntentFilter(BubblesTestActivity.BUBBLE_ACTIVITY_OPENED); - mContext.registerReceiver(receiver, filter); - - assertFalse(mBubbleController.isStackExpanded()); - - // Make ourselves foreground - Intent i = new Intent(mContext, BubblesTestActivity.class); - i.setFlags(FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(i); - - try { - latch.await(100, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } + setMetadataFlags(mRow.getEntry(), + Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, true /* enableFlag */); // Add the suppress notif bubble - mEntryListener.onPendingEntryAdded(mSuppressNotifRow.getEntry()); - mBubbleController.updateBubble(mSuppressNotifRow.getEntry()); + mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mBubbleController.updateBubble(mRow.getEntry()); - // Should NOT show in shade because we were foreground - assertFalse(mSuppressNotifRow.getEntry().showInShadeWhenBubble()); + // Notif should be suppressed because we were foreground + assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); // # of bubbles should change verify(mBubbleStateChangeListener).onHasBubblesChanged(true /* hasBubbles */); - mContext.unregisterReceiver(receiver); } @Test @@ -516,7 +515,8 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void testMarkNewNotificationAsShowInShade() { mEntryListener.onPendingEntryAdded(mRow.getEntry()); - assertTrue(mRow.getEntry().showInShadeWhenBubble()); + assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); + assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().key).showBubbleDot()); } @Test @@ -584,7 +584,7 @@ public class BubbleControllerTest extends SysuiTestCase { mBubbleController.updateBubble(mRow.getEntry()); assertTrue(mBubbleController.hasBubbles()); - assertTrue(mRow.getEntry().showInShadeWhenBubble()); + assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested( mRow.getEntry().key, REASON_CANCEL_ALL); @@ -592,7 +592,7 @@ public class BubbleControllerTest extends SysuiTestCase { // Intercept! assertTrue(intercepted); // Should update show in shade state - assertFalse(mRow.getEntry().showInShadeWhenBubble()); + assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); verify(mNotificationEntryManager, never()).performRemoveNotification( any(), anyInt()); @@ -605,7 +605,7 @@ public class BubbleControllerTest extends SysuiTestCase { mBubbleController.updateBubble(mRow.getEntry()); assertTrue(mBubbleController.hasBubbles()); - assertTrue(mRow.getEntry().showInShadeWhenBubble()); + assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested( mRow.getEntry().key, REASON_CANCEL); @@ -613,7 +613,7 @@ public class BubbleControllerTest extends SysuiTestCase { // Intercept! assertTrue(intercepted); // Should update show in shade state - assertFalse(mRow.getEntry().showInShadeWhenBubble()); + assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); verify(mNotificationEntryManager, never()).performRemoveNotification( any(), anyInt()); @@ -626,7 +626,7 @@ public class BubbleControllerTest extends SysuiTestCase { mBubbleController.updateBubble(mRow.getEntry()); assertTrue(mBubbleController.hasBubbles()); - assertTrue(mRow.getEntry().showInShadeWhenBubble()); + assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(mRow.getEntry().key)); // Dismiss the bubble mBubbleController.removeBubble(mRow.getEntry().key, BubbleController.DISMISS_USER_GESTURE); @@ -646,22 +646,21 @@ public class BubbleControllerTest extends SysuiTestCase { StatusBarWindowController statusBarWindowController, BubbleData data, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, - ZenModeController zenModeController) { + ZenModeController zenModeController, + NotificationLockscreenUserManager lockscreenUserManager, + NotificationGroupManager groupManager) { super(context, statusBarWindowController, data, Runnable::run, - configurationController, interruptionStateProvider, zenModeController); - } - - @Override - public boolean shouldAutoBubbleForFlags(Context c, NotificationEntry entry) { - return entry.notification.getNotification().getBubbleMetadata() != null; + configurationController, interruptionStateProvider, zenModeController, + lockscreenUserManager, groupManager); } } - public static class TestableNotificationInterruptionStateProvider extends + static class TestableNotificationInterruptionStateProvider extends NotificationInterruptionStateProvider { - public TestableNotificationInterruptionStateProvider(Context context) { - super(context); + TestableNotificationInterruptionStateProvider(Context context, + NotificationFilter filter, StatusBarStateController controller) { + super(context, filter, controller); mUseHeadsUp = true; } } @@ -676,4 +675,21 @@ public class BubbleControllerTest extends SysuiTestCase { .setIntent(bubbleIntent) .setIcon(Icon.createWithResource(mContext, R.drawable.android)); } + + /** + * Sets the bubble metadata flags for this entry. These flags are normally set by + * NotificationManagerService when the notification is sent, however, these tests do not + * go through that path so we set them explicitly when testing. + */ + private void setMetadataFlags(NotificationEntry entry, int flag, boolean enableFlag) { + Notification.BubbleMetadata bubbleMetadata = + entry.notification.getNotification().getBubbleMetadata(); + int flags = bubbleMetadata.getFlags(); + if (enableFlag) { + flags |= flag; + } else { + flags &= ~flag; + } + bubbleMetadata.setFlags(flags); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java index 3eea853ef2bc..238cfd78bfaf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java @@ -887,7 +887,7 @@ public class BubbleDataTest extends SysuiTestCase { private void sendUpdatedEntryAtTime(NotificationEntry entry, long postTime) { setPostTime(entry, postTime); - mBubbleData.notificationEntryUpdated(entry); + mBubbleData.notificationEntryUpdated(entry, /* suppressFlyout=*/ false); } private void changeExpandedStateAtTime(boolean shouldBeExpanded, long time) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java index 173237f7b311..a8961a85c4c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java @@ -18,7 +18,6 @@ package com.android.systemui.bubbles; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotSame; -import static junit.framework.Assert.assertTrue; import static org.mockito.Mockito.verify; @@ -46,6 +45,7 @@ import org.mockito.MockitoAnnotations; public class BubbleFlyoutViewTest extends SysuiTestCase { private BubbleFlyoutView mFlyout; private TextView mFlyoutText; + private float[] mDotCenter = new float[2]; @Before public void setUp() throws Exception { @@ -53,20 +53,25 @@ public class BubbleFlyoutViewTest extends SysuiTestCase { mFlyout = new BubbleFlyoutView(getContext()); mFlyoutText = mFlyout.findViewById(R.id.bubble_flyout_text); + mDotCenter[0] = 30; + mDotCenter[1] = 30; } @Test public void testShowFlyout_isVisible() { - mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, null); + mFlyout.setupFlyoutStartingAsDot( + "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter); + mFlyout.setVisibility(View.VISIBLE); + assertEquals("Hello", mFlyoutText.getText()); assertEquals(View.VISIBLE, mFlyout.getVisibility()); - assertEquals(1f, mFlyoutText.getAlpha(), .01f); } @Test public void testFlyoutHide_runsCallback() { Runnable after = Mockito.mock(Runnable.class); - mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, after); + mFlyout.setupFlyoutStartingAsDot( + "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter); mFlyout.hideFlyout(); verify(after).run(); @@ -74,19 +79,16 @@ public class BubbleFlyoutViewTest extends SysuiTestCase { @Test public void testSetCollapsePercent() { - mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, null); - - float initialTranslationZ = mFlyout.getTranslationZ(); + mFlyout.setupFlyoutStartingAsDot( + "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter); + mFlyout.setVisibility(View.VISIBLE); mFlyout.setCollapsePercent(1f); assertEquals(0f, mFlyoutText.getAlpha(), 0.01f); assertNotSame(0f, mFlyoutText.getTranslationX()); // Should have moved to collapse. - assertTrue(mFlyout.getTranslationZ() < initialTranslationZ); // Should be descending. mFlyout.setCollapsePercent(0f); assertEquals(1f, mFlyoutText.getAlpha(), 0.01f); assertEquals(0f, mFlyoutText.getTranslationX()); - assertEquals(initialTranslationZ, mFlyout.getTranslationZ()); - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java index 57a6aae744f0..04cf4bb4c94b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.statusbar.notification.collection; +package com.android.systemui.bubbles; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -24,6 +24,7 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.os.Bundle; import android.service.notification.NotificationListenerService.Ranking; +import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -31,6 +32,7 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; import org.junit.Before; import org.junit.Test; @@ -41,13 +43,14 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper -public class NotificationEntryTest extends SysuiTestCase { +public class BubbleTest extends SysuiTestCase { @Mock private StatusBarNotification mStatusBarNotification; @Mock private Notification mNotif; private NotificationEntry mEntry; + private Bubble mBubble; private Bundle mExtras; @Before @@ -56,11 +59,12 @@ public class NotificationEntryTest extends SysuiTestCase { when(mStatusBarNotification.getKey()).thenReturn("key"); when(mStatusBarNotification.getNotification()).thenReturn(mNotif); - + when(mStatusBarNotification.getUser()).thenReturn(new UserHandle(0)); mExtras = new Bundle(); mNotif.extras = mExtras; mEntry = NotificationEntry.buildForTest(mStatusBarNotification); + mBubble = new Bubble(mContext, mEntry); } @Test @@ -79,7 +83,7 @@ public class NotificationEntryTest extends SysuiTestCase { final String msg = "Hello there!"; doReturn(Notification.Style.class).when(mNotif).getNotificationStyle(); mExtras.putCharSequence(Notification.EXTRA_TEXT, msg); - assertEquals(msg, mEntry.getUpdateMessage(mContext)); + assertEquals(msg, mBubble.getUpdateMessage(mContext)); } @Test @@ -90,7 +94,7 @@ public class NotificationEntryTest extends SysuiTestCase { mExtras.putCharSequence(Notification.EXTRA_BIG_TEXT, msg); // Should be big text, not the small text. - assertEquals(msg, mEntry.getUpdateMessage(mContext)); + assertEquals(msg, mBubble.getUpdateMessage(mContext)); } @Test @@ -98,7 +102,7 @@ public class NotificationEntryTest extends SysuiTestCase { doReturn(Notification.MediaStyle.class).when(mNotif).getNotificationStyle(); // Media notifs don't get update messages. - assertNull(mEntry.getUpdateMessage(mContext)); + assertNull(mBubble.getUpdateMessage(mContext)); } @Test @@ -113,7 +117,7 @@ public class NotificationEntryTest extends SysuiTestCase { "Really? I prefer them that way."}); // Should be the last one only. - assertEquals("Really? I prefer them that way.", mEntry.getUpdateMessage(mContext)); + assertEquals("Really? I prefer them that way.", mBubble.getUpdateMessage(mContext)); } @Test @@ -128,6 +132,6 @@ public class NotificationEntryTest extends SysuiTestCase { "Oh, hello!", 0, "Mady").toBundle()}); // Should be the last one only. - assertEquals("Mady: Oh, hello!", mEntry.getUpdateMessage(mContext)); + assertEquals("Mady: Oh, hello!", mBubble.getUpdateMessage(mContext)); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java index b324235106c2..1fbb44346a84 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.mockito.Mockito.verify; +import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Point; import android.graphics.PointF; @@ -45,14 +46,18 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC private int mDisplayWidth = 500; private int mDisplayHeight = 1000; + private int mExpandedViewPadding = 10; + private int mOrientation = Configuration.ORIENTATION_PORTRAIT; + private float mLauncherGridDiff = 30f; @Spy private ExpandedAnimationController mExpandedController = new ExpandedAnimationController( new Point(mDisplayWidth, mDisplayHeight) /* displaySize */, - 0 /* expandedViewPadding */); + mExpandedViewPadding, mOrientation); + private int mStackOffset; - private float mBubblePadding; + private float mBubblePaddingTop; private float mBubbleSize; private PointF mExpansionPoint; @@ -65,7 +70,7 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC Resources res = mLayout.getResources(); mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); - mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); mExpansionPoint = new PointF(100, 100); } @@ -138,7 +143,6 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC assertEquals(500f, draggedBubble.getTranslationX(), 1f); assertEquals(500f, draggedBubble.getTranslationY(), 1f); - // Snap it back and make sure it made it back correctly. mLayout.removeView(draggedBubble); waitForLayoutMessageQueue(); waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); @@ -174,7 +178,7 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); - assertEquals(mBubblePadding, mViews.get(1).getTranslationX(), 1f); + assertEquals(mBubblePaddingTop, mViews.get(1).getTranslationX(), 1f); } @Test @@ -256,8 +260,8 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC * @return Bubble left x from left edge of screen. */ public float getBubbleLeft(int index) { - float bubbleLeftFromRowLeft = index * (mBubbleSize + mBubblePadding); - return getRowLeft() + bubbleLeftFromRowLeft; + final float bubbleLeft = index * (mBubbleSize + getSpaceBetweenBubbles()); + return getRowLeft() + bubbleLeft; } private float getRowLeft() { @@ -265,16 +269,29 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC return 0; } int bubbleCount = mLayout.getChildCount(); + final float totalBubbleWidth = bubbleCount * mBubbleSize; + final float totalGapWidth = (bubbleCount - 1) * getSpaceBetweenBubbles(); + final float rowWidth = totalGapWidth + totalBubbleWidth; - // Width calculations. - double bubble = bubbleCount * mBubbleSize; - float gap = (bubbleCount - 1) * mBubblePadding; - float row = gap + (float) bubble; + final float centerScreen = mDisplayWidth / 2f; + final float halfRow = rowWidth / 2f; + final float rowLeft = centerScreen - halfRow; + + return rowLeft; + } + + /** + * @return Space between bubbles in row above expanded view. + */ + private float getSpaceBetweenBubbles() { + final float rowMargins = (mExpandedViewPadding + mLauncherGridDiff) * 2; + final float maxRowWidth = mDisplayWidth - rowMargins; - float halfRow = row / 2f; - float centerScreen = mDisplayWidth / 2; - float rowLeftFromScreenLeft = centerScreen - halfRow; + final float totalBubbleWidth = mMaxBubbles * mBubbleSize; + final float totalGapWidth = maxRowWidth - totalBubbleWidth; - return rowLeftFromScreenLeft; + final int gapCount = mMaxBubbles - 1; + final float gapWidth = totalGapWidth / gapCount; + return gapWidth; } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java index f8b32c213109..86554b1d71aa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java @@ -156,8 +156,8 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { }); // Set end listeners for both x and y. - mLayout.setEndActionForProperty(xEndAction, DynamicAnimation.TRANSLATION_X); - mLayout.setEndActionForProperty(yEndAction, DynamicAnimation.TRANSLATION_Y); + mTestableController.setEndActionForProperty(xEndAction, DynamicAnimation.TRANSLATION_X); + mTestableController.setEndActionForProperty(yEndAction, DynamicAnimation.TRANSLATION_Y); // Animate x, and wait for it to finish. mTestableController.animationForChildAtIndex(0) @@ -190,7 +190,7 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { }); // Set the end listener. - mLayout.setEndActionForProperty(xEndListener, DynamicAnimation.TRANSLATION_X); + mTestableController.setEndActionForProperty(xEndListener, DynamicAnimation.TRANSLATION_X); // Animate x, and wait for it to finish. mTestableController.animationForChildAtIndex(0) @@ -205,7 +205,7 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { mTestableController.animationForChildAtIndex(0) .translationX(1000) .start(); - mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X); + mTestableController.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X); xLatch.await(1, TimeUnit.SECONDS); // Make sure the end listener was not called. diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java index f633f3996d13..a5f2e8b3db37 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java @@ -68,7 +68,7 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase { @Mock private DisplayCutout mCutout; - private int mMaxBubbles; + protected int mMaxBubbles; @Before public void setUp() throws Exception { @@ -195,14 +195,13 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase { private void setTestEndActionForProperty( Runnable action, DynamicAnimation.ViewProperty property) { final Runnable realEndAction = mEndActionForProperty.get(property); - - setEndActionForProperty(() -> { + mLayout.mEndActionForProperty.put(property, () -> { if (realEndAction != null) { realEndAction.run(); } action.run(); - }, property); + }); } /** PhysicsPropertyAnimator that posts its animations to the main thread. */ @@ -219,6 +218,11 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase { property, view, value, startVel, startDelay, stiffness, dampingRatio, afterCallbacks)); } + + @Override + protected void startPathAnimation() { + mMainThreadHandler.post(super::startPathAnimation); + } } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java index 31a7d5a45b68..d79128ca5c78 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java @@ -339,10 +339,10 @@ public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase @Override protected void springFirstBubbleWithStackFollowing(DynamicAnimation.ViewProperty property, - SpringForce spring, float vel, float finalPosition) { + SpringForce spring, float vel, float finalPosition, Runnable... after) { mMainThreadHandler.post(() -> super.springFirstBubbleWithStackFollowing( - property, spring, vel, finalPosition)); + property, spring, vel, finalPosition, after)); } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInterruptionStateProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInterruptionStateProviderTest.java new file mode 100644 index 000000000000..b044595e5a8e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInterruptionStateProviderTest.java @@ -0,0 +1,589 @@ +/* + * Copyright (C) 2019 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; + + +import static android.app.Notification.FLAG_BUBBLE; +import static android.app.NotificationManager.IMPORTANCE_DEFAULT; +import static android.app.NotificationManager.IMPORTANCE_HIGH; +import static android.app.NotificationManager.IMPORTANCE_LOW; +import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; +import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; + +import static com.android.systemui.statusbar.StatusBarState.SHADE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.hardware.display.AmbientDisplayConfiguration; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.UserHandle; +import android.service.dreams.IDreamManager; +import android.service.notification.StatusBarNotification; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.notification.NotificationFilter; +import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.policy.HeadsUpManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for the interruption state provider which understands whether the system & notification + * is in a state allowing a particular notification to hun, pulse, or bubble. + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class NotificationInterruptionStateProviderTest extends SysuiTestCase { + + @Mock + PowerManager mPowerManager; + @Mock + IDreamManager mDreamManager; + @Mock + AmbientDisplayConfiguration mAmbientDisplayConfiguration; + @Mock + NotificationFilter mNotificationFilter; + @Mock + StatusBarStateController mStatusBarStateController; + @Mock + NotificationPresenter mPresenter; + @Mock + HeadsUpManager mHeadsUpManager; + @Mock + NotificationInterruptionStateProvider.HeadsUpSuppressor mHeadsUpSuppressor; + + private NotificationInterruptionStateProvider mNotifInterruptionStateProvider; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mNotifInterruptionStateProvider = + new TestableNotificationInterruptionStateProvider(mContext, + mPowerManager, + mDreamManager, + mAmbientDisplayConfiguration, + mNotificationFilter, + mStatusBarStateController); + + mNotifInterruptionStateProvider.setUpWithPresenter( + mPresenter, + mHeadsUpManager, + mHeadsUpSuppressor); + } + + /** + * Sets up the state such that any requests to + * {@link NotificationInterruptionStateProvider#canAlertCommon(NotificationEntry)} will + * pass as long its provided NotificationEntry fulfills group suppression check. + */ + private void ensureStateForAlertCommon() { + when(mNotificationFilter.shouldFilterOut(any())).thenReturn(false); + } + + /** + * Sets up the state such that any requests to + * {@link NotificationInterruptionStateProvider#canAlertAwakeCommon(NotificationEntry)} will + * pass as long its provided NotificationEntry fulfills launch fullscreen check. + */ + private void ensureStateForAlertAwakeCommon() { + when(mPresenter.isDeviceInVrMode()).thenReturn(false); + when(mHeadsUpManager.isSnoozed(any())).thenReturn(false); + } + + /** + * Sets up the state such that any requests to + * {@link NotificationInterruptionStateProvider#shouldHeadsUp(NotificationEntry)} will + * pass as long its provided NotificationEntry fulfills importance & DND checks. + */ + private void ensureStateForHeadsUpWhenAwake() throws RemoteException { + ensureStateForAlertCommon(); + ensureStateForAlertAwakeCommon(); + + when(mStatusBarStateController.isDozing()).thenReturn(false); + when(mDreamManager.isDreaming()).thenReturn(false); + when(mPowerManager.isScreenOn()).thenReturn(true); + when(mHeadsUpSuppressor.canHeadsUp(any(), any())).thenReturn(true); + } + + /** + * Sets up the state such that any requests to + * {@link NotificationInterruptionStateProvider#shouldHeadsUp(NotificationEntry)} will + * pass as long its provided NotificationEntry fulfills importance & DND checks. + */ + private void ensureStateForHeadsUpWhenDozing() { + ensureStateForAlertCommon(); + + when(mStatusBarStateController.isDozing()).thenReturn(true); + when(mAmbientDisplayConfiguration.pulseOnNotificationEnabled(anyInt())).thenReturn(true); + } + + /** + * Sets up the state such that any requests to + * {@link NotificationInterruptionStateProvider#shouldBubbleUp(NotificationEntry)} will + * pass as long its provided NotificationEntry fulfills importance & bubble checks. + */ + private void ensureStateForBubbleUp() { + ensureStateForAlertCommon(); + ensureStateForAlertAwakeCommon(); + } + + /** + * Ensure that the disabled state is set correctly. + */ + @Test + public void testDisableNotificationAlerts() { + // Enabled by default + assertThat(mNotifInterruptionStateProvider.areNotificationAlertsDisabled()).isFalse(); + + // Disable alerts + mNotifInterruptionStateProvider.setDisableNotificationAlerts(true); + assertThat(mNotifInterruptionStateProvider.areNotificationAlertsDisabled()).isTrue(); + + // Enable alerts + mNotifInterruptionStateProvider.setDisableNotificationAlerts(false); + assertThat(mNotifInterruptionStateProvider.areNotificationAlertsDisabled()).isFalse(); + } + + /** + * Ensure that the disabled alert state effects whether HUNs are enabled. + */ + @Test + public void testHunSettingsChange_enabled_butAlertsDisabled() { + // Set up but without a mock change observer + mNotifInterruptionStateProvider.setUpWithPresenter( + mPresenter, + mHeadsUpManager, + mHeadsUpSuppressor); + + // HUNs enabled by default + assertThat(mNotifInterruptionStateProvider.getUseHeadsUp()).isTrue(); + + // Set alerts disabled + mNotifInterruptionStateProvider.setDisableNotificationAlerts(true); + + // No more HUNs + assertThat(mNotifInterruptionStateProvider.getUseHeadsUp()).isFalse(); + } + + /** + * Alerts can happen. + */ + @Test + public void testCanAlertCommon_true() { + ensureStateForAlertCommon(); + + NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); + assertThat(mNotifInterruptionStateProvider.canAlertCommon(entry)).isTrue(); + } + + /** + * Filtered out notifications don't alert. + */ + @Test + public void testCanAlertCommon_false_filteredOut() { + ensureStateForAlertCommon(); + when(mNotificationFilter.shouldFilterOut(any())).thenReturn(true); + + NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); + assertThat(mNotifInterruptionStateProvider.canAlertCommon(entry)).isFalse(); + } + + /** + * Grouped notifications have different alerting behaviours, sometimes the alert for a + * grouped notification may be suppressed {@link android.app.Notification#GROUP_ALERT_CHILDREN}. + */ + @Test + public void testCanAlertCommon_false_suppressedForGroups() { + ensureStateForAlertCommon(); + + Notification n = new Notification.Builder(getContext(), "a") + .setGroup("a") + .setGroupSummary(true) + .setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN) + .build(); + StatusBarNotification sbn = new StatusBarNotification("a", "a", 0, "a", 0, 0, n, + UserHandle.of(0), null, 0); + NotificationEntry entry = NotificationEntry.buildForTest(sbn); + entry.importance = IMPORTANCE_DEFAULT; + + assertThat(mNotifInterruptionStateProvider.canAlertCommon(entry)).isFalse(); + } + + /** + * HUNs while dozing can happen. + */ + @Test + public void testShouldHeadsUpWhenDozing_true() { + ensureStateForHeadsUpWhenDozing(); + + NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue(); + } + + /** + * Ambient display can show HUNs for new notifications, this may be disabled. + */ + @Test + public void testShouldHeadsUpWhenDozing_false_pulseDisabled() { + ensureStateForHeadsUpWhenDozing(); + when(mAmbientDisplayConfiguration.pulseOnNotificationEnabled(anyInt())).thenReturn(false); + + NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + } + + /** + * If the device is not in ambient display or sleeping then we don't HUN. + */ + @Test + public void testShouldHeadsUpWhenDozing_false_notDozing() { + ensureStateForHeadsUpWhenDozing(); + when(mStatusBarStateController.isDozing()).thenReturn(false); + + NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + } + + /** + * In DND ambient effects can be suppressed + * {@link android.app.NotificationManager.Policy#SUPPRESSED_EFFECT_AMBIENT}. + */ + @Test + public void testShouldHeadsUpWhenDozing_false_suppressingAmbient() { + ensureStateForHeadsUpWhenDozing(); + + NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); + entry.suppressedVisualEffects = SUPPRESSED_EFFECT_AMBIENT; + + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + } + + /** + * Notifications that are < {@link android.app.NotificationManager#IMPORTANCE_DEFAULT} don't + * get to pulse. + */ + @Test + public void testShouldHeadsUpWhenDozing_false_lessImportant() { + ensureStateForHeadsUpWhenDozing(); + + NotificationEntry entry = createNotification(IMPORTANCE_LOW); + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + } + + /** + * Heads up can happen. + */ + @Test + public void testShouldHeadsUp_true() throws RemoteException { + ensureStateForHeadsUpWhenAwake(); + + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue(); + } + + /** + * Heads up notifications can be disabled in general. + */ + @Test + public void testShouldHeadsUp_false_noHunsAllowed() throws RemoteException { + ensureStateForHeadsUpWhenAwake(); + + // Set alerts disabled, this should cause heads up to be false + mNotifInterruptionStateProvider.setDisableNotificationAlerts(true); + assertThat(mNotifInterruptionStateProvider.getUseHeadsUp()).isFalse(); + + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + } + + /** + * If the device is dozing, we don't show as heads up. + */ + @Test + public void testShouldHeadsUp_false_dozing() throws RemoteException { + ensureStateForHeadsUpWhenAwake(); + when(mStatusBarStateController.isDozing()).thenReturn(true); + + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + } + + /** + * If the notification is a bubble, and the user is not on AOD / lockscreen, then + * the bubble is shown rather than the heads up. + */ + @Test + public void testShouldHeadsUp_false_bubble() throws RemoteException { + ensureStateForHeadsUpWhenAwake(); + + // Bubble bit only applies to interruption when we're in the shade + when(mStatusBarStateController.getState()).thenReturn(SHADE); + + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(createBubble())).isFalse(); + } + + /** + * If we're not allowed to alert in general, we shouldn't be shown as heads up. + */ + @Test + public void testShouldHeadsUp_false_alertCommonFalse() throws RemoteException { + ensureStateForHeadsUpWhenAwake(); + // Make canAlertCommon false by saying it's filtered out + when(mNotificationFilter.shouldFilterOut(any())).thenReturn(true); + + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + } + + /** + * In DND HUN peek effects can be suppressed + * {@link android.app.NotificationManager.Policy#SUPPRESSED_EFFECT_PEEK}. + */ + @Test + public void testShouldHeadsUp_false_suppressPeek() throws RemoteException { + ensureStateForHeadsUpWhenAwake(); + + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + entry.suppressedVisualEffects = SUPPRESSED_EFFECT_PEEK; + + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + } + + /** + * Notifications that are < {@link android.app.NotificationManager#IMPORTANCE_HIGH} don't get + * to show as a heads up. + */ + @Test + public void testShouldHeadsUp_false_lessImportant() throws RemoteException { + ensureStateForHeadsUpWhenAwake(); + + NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + } + + /** + * If the device is not in use then we shouldn't be shown as heads up. + */ + @Test + public void testShouldHeadsUp_false_deviceNotInUse() throws RemoteException { + ensureStateForHeadsUpWhenAwake(); + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + + // Device is not in use if screen is not on + when(mPowerManager.isScreenOn()).thenReturn(false); + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + + // Also not in use if screen is on but we're showing screen saver / "dreaming" + when(mPowerManager.isDeviceIdleMode()).thenReturn(true); + when(mDreamManager.isDreaming()).thenReturn(true); + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + } + + /** + * If something wants to suppress this heads up, then it shouldn't be shown as a heads up. + */ + @Test + public void testShouldHeadsUp_false_suppressed() throws RemoteException { + ensureStateForHeadsUpWhenAwake(); + when(mHeadsUpSuppressor.canHeadsUp(any(), any())).thenReturn(false); + + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + verify(mHeadsUpSuppressor).canHeadsUp(any(), any()); + } + + /** + * On screen alerts don't happen when the device is in VR Mode. + */ + @Test + public void testCanAlertAwakeCommon__false_vrMode() { + ensureStateForAlertAwakeCommon(); + when(mPresenter.isDeviceInVrMode()).thenReturn(true); + + NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); + assertThat(mNotifInterruptionStateProvider.canAlertAwakeCommon(entry)).isFalse(); + } + + /** + * On screen alerts don't happen when the notification is snoozed. + */ + @Test + public void testCanAlertAwakeCommon_false_snoozedPackage() { + ensureStateForAlertAwakeCommon(); + when(mHeadsUpManager.isSnoozed(any())).thenReturn(true); + + NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); + assertThat(mNotifInterruptionStateProvider.canAlertAwakeCommon(entry)).isFalse(); + } + + /** + * On screen alerts don't happen when that package has just launched fullscreen. + */ + @Test + public void testCanAlertAwakeCommon_false_justLaunchedFullscreen() { + ensureStateForAlertAwakeCommon(); + + NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); + entry.notifyFullScreenIntentLaunched(); + + assertThat(mNotifInterruptionStateProvider.canAlertAwakeCommon(entry)).isFalse(); + } + + /** + * Bubbles can happen. + */ + @Test + public void testShouldBubbleUp_true() { + ensureStateForBubbleUp(); + assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(createBubble())).isTrue(); + } + + /** + * If the notification doesn't have permission to bubble, it shouldn't bubble. + */ + @Test + public void shouldBubbleUp_false_notAllowedToBubble() { + ensureStateForBubbleUp(); + + NotificationEntry entry = createBubble(); + entry.canBubble = false; + + assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(entry)).isFalse(); + } + + /** + * If the notification isn't a bubble, it should definitely not show as a bubble. + */ + @Test + public void shouldBubbleUp_false_notABubble() { + ensureStateForBubbleUp(); + + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + entry.canBubble = true; + + assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(entry)).isFalse(); + } + + /** + * If the notification doesn't have bubble metadata, it shouldn't bubble. + */ + @Test + public void shouldBubbleUp_false_invalidMetadata() { + ensureStateForBubbleUp(); + + NotificationEntry entry = createNotification(IMPORTANCE_HIGH); + entry.canBubble = true; + entry.notification.getNotification().flags |= FLAG_BUBBLE; + + assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(entry)).isFalse(); + } + + /** + * If the notification can't heads up in general, it shouldn't bubble. + */ + @Test + public void shouldBubbleUp_false_alertAwakeCommonFalse() { + ensureStateForBubbleUp(); + + // Make alert common return false by pretending we're in VR mode + when(mPresenter.isDeviceInVrMode()).thenReturn(true); + + assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(createBubble())).isFalse(); + } + + /** + * If the notification can't heads up in general, it shouldn't bubble. + */ + @Test + public void shouldBubbleUp_false_alertCommonFalse() { + ensureStateForBubbleUp(); + + // Make canAlertCommon false by saying it's filtered out + when(mNotificationFilter.shouldFilterOut(any())).thenReturn(true); + + assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(createBubble())).isFalse(); + } + + private NotificationEntry createBubble() { + Notification.BubbleMetadata data = new Notification.BubbleMetadata.Builder() + .setIntent(PendingIntent.getActivity(mContext, 0, new Intent(), 0)) + .setIcon(Icon.createWithResource(mContext.getResources(), R.drawable.android)) + .build(); + Notification n = new Notification.Builder(getContext(), "a") + .setContentTitle("title") + .setContentText("content text") + .setBubbleMetadata(data) + .build(); + StatusBarNotification sbn = new StatusBarNotification("a", "a", 0, "a", 0, 0, n, + UserHandle.of(0), null, 0); + NotificationEntry entry = NotificationEntry.buildForTest(sbn); + entry.notification.getNotification().flags |= FLAG_BUBBLE; + entry.importance = IMPORTANCE_HIGH; + entry.canBubble = true; + return entry; + } + + private NotificationEntry createNotification(int importance) { + Notification n = new Notification.Builder(getContext(), "a") + .setContentTitle("title") + .setContentText("content text") + .build(); + StatusBarNotification sbn = new StatusBarNotification("a", "a", 0, "a", 0, 0, n, + UserHandle.of(0), null, 0); + NotificationEntry entry = NotificationEntry.buildForTest(sbn); + entry.importance = importance; + return entry; + } + + /** + * Testable class overriding constructor. + */ + public class TestableNotificationInterruptionStateProvider extends + NotificationInterruptionStateProvider { + + TestableNotificationInterruptionStateProvider(Context context, + PowerManager powerManager, IDreamManager dreamManager, + AmbientDisplayConfiguration ambientDisplayConfiguration, + NotificationFilter notificationFilter, + StatusBarStateController statusBarStateController) { + super(context, powerManager, dreamManager, ambientDisplayConfiguration, + notificationFilter, + statusBarStateController); + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java index 54d8688857fa..388cf582237b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java @@ -41,7 +41,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.Dependency; import com.android.systemui.InitController; import com.android.systemui.SysuiTestCase; -import com.android.systemui.bubbles.BubbleData; +import com.android.systemui.bubbles.BubbleController; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationEntryManager; @@ -110,8 +110,9 @@ public class NotificationViewHierarchyManagerTest extends SysuiTestCase { mViewHierarchyManager = new NotificationViewHierarchyManager(mContext, mHandler, mLockscreenUserManager, mGroupManager, mVisualStabilityManager, mock(StatusBarStateControllerImpl.class), mEntryManager, - () -> mShadeController, new BubbleData(mContext), + () -> mShadeController, mock(KeyguardBypassController.class), + mock(BubbleController.class), mock(DynamicPrivacyController.class)); Dependency.get(InitController.class).executePostInitTasks(); mViewHierarchyManager.setUpWithPresenter(mPresenter, mListContainer); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 97ad47ec3f0c..c5d4019252ff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.phone; -import static com.android.systemui.statusbar.phone.ScrimController.VISIBILITY_FULLY_OPAQUE; -import static com.android.systemui.statusbar.phone.ScrimController.VISIBILITY_FULLY_TRANSPARENT; -import static com.android.systemui.statusbar.phone.ScrimController.VISIBILITY_SEMI_TRANSPARENT; +import static com.android.systemui.statusbar.phone.ScrimController.OPAQUE; +import static com.android.systemui.statusbar.phone.ScrimController.SEMI_TRANSPARENT; +import static com.android.systemui.statusbar.phone.ScrimController.TRANSPARENT; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -69,6 +69,7 @@ public class ScrimControllerTest extends SysuiTestCase { private SynchronousScrimController mScrimController; private ScrimView mScrimBehind; private ScrimView mScrimInFront; + private ScrimView mScrimForBubble; private ScrimState mScrimState; private float mScrimBehindAlpha; private GradientColors mScrimInFrontColor; @@ -84,6 +85,7 @@ public class ScrimControllerTest extends SysuiTestCase { public void setup() { mScrimBehind = spy(new ScrimView(getContext())); mScrimInFront = new ScrimView(getContext()); + mScrimForBubble = new ScrimView(getContext()); mWakeLock = mock(WakeLock.class); mAlarmManager = mock(AlarmManager.class); mAlwaysOnEnabled = true; @@ -92,6 +94,7 @@ public class ScrimControllerTest extends SysuiTestCase { when(mDozeParamenters.getAlwaysOn()).thenAnswer(invocation -> mAlwaysOnEnabled); when(mDozeParamenters.getDisplayNeedsBlanking()).thenReturn(true); mScrimController = new SynchronousScrimController(mScrimBehind, mScrimInFront, + mScrimForBubble, (scrimState, scrimBehindAlpha, scrimInFrontColor) -> { mScrimState = scrimState; mScrimBehindAlpha = scrimBehindAlpha; @@ -114,21 +117,28 @@ public class ScrimControllerTest extends SysuiTestCase { public void transitionToKeyguard() { mScrimController.transitionTo(ScrimState.KEYGUARD); mScrimController.finishAnimationsImmediately(); - // Front scrim should be transparent - // Back scrim should be visible without tint - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_SEMI_TRANSPARENT); - assertScrimTint(mScrimBehind, true /* tinted */); + + assertScrimAlpha(TRANSPARENT /* front */, + SEMI_TRANSPARENT /* back */, + TRANSPARENT /* bubble */); + + assertScrimTint(true /* front */, + true /* behind */, + false /* bubble */); } @Test public void transitionToAod_withRegularWallpaper() { mScrimController.transitionTo(ScrimState.AOD); mScrimController.finishAnimationsImmediately(); - // Front scrim should be transparent - // Back scrim should be visible with tint - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_FULLY_OPAQUE); - assertScrimTint(mScrimBehind, true /* tinted */); - assertScrimTint(mScrimInFront, true /* tinted */); + + assertScrimAlpha(TRANSPARENT /* front */, + OPAQUE /* back */, + TRANSPARENT /* bubble */); + + assertScrimTint(true /* front */, + true /* behind */, + false /* bubble */); } @Test @@ -136,14 +146,18 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.setWallpaperSupportsAmbientMode(true); mScrimController.transitionTo(ScrimState.AOD); mScrimController.finishAnimationsImmediately(); - // Front scrim should be transparent - // Back scrim should be transparent - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_FULLY_TRANSPARENT); + + assertScrimAlpha(TRANSPARENT /* front */, + TRANSPARENT /* back */, + TRANSPARENT /* bubble */); // Pulsing notification should conserve AOD wallpaper. mScrimController.transitionTo(ScrimState.PULSING); mScrimController.finishAnimationsImmediately(); - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_FULLY_TRANSPARENT); + + assertScrimAlpha(TRANSPARENT /* front */, + TRANSPARENT /* back */, + TRANSPARENT /* bubble */); } @Test @@ -152,11 +166,14 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.setWallpaperSupportsAmbientMode(true); mScrimController.transitionTo(ScrimState.AOD); mScrimController.finishAnimationsImmediately(); - // Front scrim should be transparent - // Back scrim should be visible with tint - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_FULLY_OPAQUE); - assertScrimTint(mScrimBehind, true /* tinted */); - assertScrimTint(mScrimInFront, true /* tinted */); + + assertScrimAlpha(TRANSPARENT /* front */, + OPAQUE /* back */, + TRANSPARENT /* bubble */); + + assertScrimTint(true /* front */, + true /* behind */, + false /* bubble */); } @Test @@ -166,11 +183,14 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.finishAnimationsImmediately(); mScrimController.setHasBackdrop(true); mScrimController.finishAnimationsImmediately(); - // Front scrim should be transparent - // Back scrim should be visible with tint - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_FULLY_OPAQUE); - assertScrimTint(mScrimBehind, true /* tinted */); - assertScrimTint(mScrimInFront, true /* tinted */); + + assertScrimAlpha(TRANSPARENT /* front */, + OPAQUE /* back */, + TRANSPARENT /* bubble */); + + assertScrimTint(true /* front */, + true /* behind */, + false /* bubble */); } @Test @@ -179,27 +199,32 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.transitionTo(ScrimState.KEYGUARD); mScrimController.setAodFrontScrimAlpha(0.5f); mScrimController.finishAnimationsImmediately(); - // Front scrim should be transparent - // Back scrim should be visible without tint - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_SEMI_TRANSPARENT); + + assertScrimAlpha(TRANSPARENT /* front */, + SEMI_TRANSPARENT /* back */, + TRANSPARENT /* bubble */); // ... but that it does take effect once we enter the AOD state. mScrimController.transitionTo(ScrimState.AOD); mScrimController.finishAnimationsImmediately(); - // Front scrim should be semi-transparent - // Back scrim should be visible - assertScrimVisibility(VISIBILITY_SEMI_TRANSPARENT, VISIBILITY_FULLY_OPAQUE); + assertScrimAlpha(SEMI_TRANSPARENT /* front */, + OPAQUE /* back */, + TRANSPARENT /* bubble */); // ... and that if we set it while we're in AOD, it does take immediate effect. mScrimController.setAodFrontScrimAlpha(1f); - assertScrimVisibility(VISIBILITY_FULLY_OPAQUE, VISIBILITY_FULLY_OPAQUE); + assertScrimAlpha(OPAQUE /* front */, + OPAQUE /* back */, + TRANSPARENT /* bubble */); // ... and make sure we recall the previous front scrim alpha even if we transition away // for a bit. mScrimController.transitionTo(ScrimState.UNLOCKED); mScrimController.transitionTo(ScrimState.AOD); mScrimController.finishAnimationsImmediately(); - assertScrimVisibility(VISIBILITY_FULLY_OPAQUE, VISIBILITY_FULLY_OPAQUE); + assertScrimAlpha(OPAQUE /* front */, + OPAQUE /* back */, + TRANSPARENT /* bubble */); // ... and alpha updates should be completely ignored if always_on is off. // Passing it forward would mess up the wake-up transition. @@ -223,20 +248,28 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.setWallpaperSupportsAmbientMode(false); mScrimController.transitionTo(ScrimState.AOD); mScrimController.finishAnimationsImmediately(); - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_FULLY_OPAQUE); + assertScrimAlpha(TRANSPARENT /* front */, + OPAQUE /* back */, + TRANSPARENT /* bubble */); mScrimController.transitionTo(ScrimState.PULSING); mScrimController.finishAnimationsImmediately(); // Front scrim should be transparent, but tinted // Back scrim should be semi-transparent so the user can see the wallpaper // Pulse callback should have been invoked - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_FULLY_OPAQUE); - assertScrimTint(mScrimBehind, true /* tinted */); - assertScrimTint(mScrimInFront, true /* tinted */); + assertScrimAlpha(TRANSPARENT /* front */, + OPAQUE /* back */, + TRANSPARENT /* bubble */); + + assertScrimTint(true /* front */, + true /* behind */, + false /* bubble */); mScrimController.setWakeLockScreenSensorActive(true); mScrimController.finishAnimationsImmediately(); - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_SEMI_TRANSPARENT); + assertScrimAlpha(TRANSPARENT /* front */, + SEMI_TRANSPARENT /* back */, + TRANSPARENT /* bubble */); } @Test @@ -245,8 +278,13 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.finishAnimationsImmediately(); // Front scrim should be transparent // Back scrim should be visible without tint - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_SEMI_TRANSPARENT); - assertScrimTint(mScrimBehind, false /* tinted */); + assertScrimAlpha(TRANSPARENT /* front */, + SEMI_TRANSPARENT /* back */, + TRANSPARENT /* bubble */); + + assertScrimTint(false /* front */, + false /* behind */, + false /* bubble */); } @Test @@ -255,8 +293,12 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.finishAnimationsImmediately(); // Front scrim should be transparent // Back scrim should be visible without tint - assertScrimVisibility(VISIBILITY_SEMI_TRANSPARENT, VISIBILITY_FULLY_TRANSPARENT); - assertScrimTint(mScrimBehind, false /* tinted */); + assertScrimAlpha(SEMI_TRANSPARENT /* front */, + TRANSPARENT /* back */, + TRANSPARENT /* bubble */); + assertScrimTint(false /* front */, + false /* behind */, + false /* bubble */); } @Test @@ -264,15 +306,19 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.setPanelExpansion(0f); mScrimController.transitionTo(ScrimState.UNLOCKED); mScrimController.finishAnimationsImmediately(); - // Front scrim should be transparent - // Back scrim should be transparent - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_FULLY_TRANSPARENT); - assertScrimTint(mScrimBehind, false /* tinted */); - assertScrimTint(mScrimInFront, false /* tinted */); + assertScrimAlpha(TRANSPARENT /* front */, + TRANSPARENT /* back */, + TRANSPARENT /* bubble */); + + assertScrimTint(false /* front */, + false /* behind */, + false /* bubble */); // Back scrim should be visible after start dragging mScrimController.setPanelExpansion(0.5f); - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_SEMI_TRANSPARENT); + assertScrimAlpha(TRANSPARENT /* front */, + SEMI_TRANSPARENT /* back */, + TRANSPARENT /* bubble */); } @Test @@ -280,12 +326,19 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.transitionTo(ScrimState.BUBBLE_EXPANDED); mScrimController.finishAnimationsImmediately(); + assertScrimTint(false /* front */, + false /* behind */, + false /* bubble */); + // Front scrim should be transparent - Assert.assertEquals(ScrimController.VISIBILITY_FULLY_TRANSPARENT, + Assert.assertEquals(ScrimController.TRANSPARENT, mScrimInFront.getViewAlpha(), 0.0f); // Back scrim should be visible Assert.assertEquals(ScrimController.GRADIENT_SCRIM_ALPHA_BUSY, mScrimBehind.getViewAlpha(), 0.0f); + // Bubble scrim should be visible + Assert.assertEquals(ScrimController.GRADIENT_SCRIM_ALPHA_BUSY, + mScrimBehind.getViewAlpha(), 0.0f); } @Test @@ -354,16 +407,22 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.setPanelExpansion(0f); mScrimController.finishAnimationsImmediately(); mScrimController.transitionTo(ScrimState.UNLOCKED); - // Immediately tinted after the transition starts - assertScrimTint(mScrimInFront, true /* tinted */); - assertScrimTint(mScrimBehind, true /* tinted */); + + // Immediately tinted black after the transition starts + assertScrimTint(true /* front */, + true /* behind */, + true /* bubble */); + mScrimController.finishAnimationsImmediately(); - // Front scrim should be transparent - // Back scrim should be transparent - // Neither scrims should be tinted anymore after the animation. - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_FULLY_TRANSPARENT); - assertScrimTint(mScrimInFront, false /* tinted */); - assertScrimTint(mScrimBehind, false /* tinted */); + + // All scrims should be transparent at the end of fade transition. + assertScrimAlpha(TRANSPARENT /* front */, + TRANSPARENT /* behind */, + TRANSPARENT /* bubble */); + + assertScrimTint(false /* front */, + false /* behind */, + false /* bubble */); } @Test @@ -378,9 +437,11 @@ public class ScrimControllerTest extends SysuiTestCase { // Front scrim should be black in the middle of the transition Assert.assertTrue("Scrim should be visible during transition. Alpha: " + mScrimInFront.getViewAlpha(), mScrimInFront.getViewAlpha() > 0); - assertScrimTint(mScrimInFront, true /* tinted */); + assertScrimTint(true /* front */, + true /* behind */, + true /* bubble */); Assert.assertSame("Scrim should be visible during transition.", - mScrimVisibility, VISIBILITY_FULLY_OPAQUE); + mScrimVisibility, OPAQUE); } }); mScrimController.finishAnimationsImmediately(); @@ -588,11 +649,15 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.setKeyguardOccluded(true); mScrimController.transitionTo(ScrimState.AOD); mScrimController.finishAnimationsImmediately(); - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_FULLY_OPAQUE); + assertScrimAlpha(TRANSPARENT /* front */, + OPAQUE /* behind */, + TRANSPARENT /* bubble */); mScrimController.transitionTo(ScrimState.PULSING); mScrimController.finishAnimationsImmediately(); - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_FULLY_OPAQUE); + assertScrimAlpha(TRANSPARENT /* front */, + OPAQUE /* behind */, + TRANSPARENT /* bubble */); } @Test @@ -600,11 +665,15 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.setWallpaperSupportsAmbientMode(true); mScrimController.transitionTo(ScrimState.AOD); mScrimController.finishAnimationsImmediately(); - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_FULLY_TRANSPARENT); + assertScrimAlpha(TRANSPARENT /* front */, + TRANSPARENT /* behind */, + TRANSPARENT /* bubble */); mScrimController.setKeyguardOccluded(true); mScrimController.finishAnimationsImmediately(); - assertScrimVisibility(VISIBILITY_FULLY_TRANSPARENT, VISIBILITY_FULLY_OPAQUE); + assertScrimAlpha(TRANSPARENT /* front */, + OPAQUE /* behind */, + TRANSPARENT /* bubble */); } @Test @@ -643,33 +712,68 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimInFront.getDefaultFocusHighlightEnabled()); Assert.assertFalse("Scrim shouldn't have focus highlight", mScrimBehind.getDefaultFocusHighlightEnabled()); + Assert.assertFalse("Scrim shouldn't have focus highlight", + mScrimForBubble.getDefaultFocusHighlightEnabled()); } - private void assertScrimTint(ScrimView scrimView, boolean tinted) { - final boolean viewIsTinted = scrimView.getTint() != Color.TRANSPARENT; - final String name = scrimView == mScrimInFront ? "front" : "back"; + private void assertScrimTint(boolean front, boolean behind, boolean bubble) { + Assert.assertEquals("Tint test failed at state " + mScrimController.getState() + + " with scrim: " + getScrimName(mScrimInFront) + " and tint: " + + Integer.toHexString(mScrimInFront.getTint()), + front, mScrimInFront.getTint() != Color.TRANSPARENT); + Assert.assertEquals("Tint test failed at state " + mScrimController.getState() - +" with scrim: " + name + " and tint: " + Integer.toHexString(scrimView.getTint()), - tinted, viewIsTinted); + + " with scrim: " + getScrimName(mScrimBehind) + " and tint: " + + Integer.toHexString(mScrimBehind.getTint()), + behind, mScrimBehind.getTint() != Color.TRANSPARENT); + + Assert.assertEquals("Tint test failed at state " + mScrimController.getState() + + " with scrim: " + getScrimName(mScrimForBubble) + " and tint: " + + Integer.toHexString(mScrimForBubble.getTint()), + bubble, mScrimForBubble.getTint() != Color.TRANSPARENT); + } + + private String getScrimName(ScrimView scrim) { + if (scrim == mScrimInFront) { + return "front"; + } else if (scrim == mScrimBehind) { + return "back"; + } else if (scrim == mScrimForBubble) { + return "bubble"; + } + return "unknown_scrim"; } - private void assertScrimVisibility(int inFront, int behind) { - boolean inFrontVisible = inFront != ScrimController.VISIBILITY_FULLY_TRANSPARENT; - boolean behindVisible = behind != ScrimController.VISIBILITY_FULLY_TRANSPARENT; - Assert.assertEquals("Unexpected front scrim visibility. Alpha is " - + mScrimInFront.getViewAlpha(), inFrontVisible, mScrimInFront.getViewAlpha() > 0); - Assert.assertEquals("Unexpected back scrim visibility. Alpha is " - + mScrimBehind.getViewAlpha(), behindVisible, mScrimBehind.getViewAlpha() > 0); + private void assertScrimAlpha(int front, int behind, int bubble) { + // Check single scrim visibility. + Assert.assertEquals("Unexpected front scrim alpha: " + + mScrimInFront.getViewAlpha(), + front != TRANSPARENT /* expected */, + mScrimInFront.getViewAlpha() > TRANSPARENT /* actual */); + + Assert.assertEquals("Unexpected back scrim alpha: " + + mScrimBehind.getViewAlpha(), + behind != TRANSPARENT /* expected */, + mScrimBehind.getViewAlpha() > TRANSPARENT /* actual */); + + Assert.assertEquals( + "Unexpected bubble scrim alpha: " + + mScrimForBubble.getViewAlpha(), /* message */ + bubble != TRANSPARENT /* expected */, + mScrimForBubble.getViewAlpha() > TRANSPARENT /* actual */); + // Check combined scrim visibility. final int visibility; - if (inFront == VISIBILITY_FULLY_OPAQUE || behind == VISIBILITY_FULLY_OPAQUE) { - visibility = VISIBILITY_FULLY_OPAQUE; - } else if (inFront > VISIBILITY_FULLY_TRANSPARENT || behind > VISIBILITY_FULLY_TRANSPARENT) { - visibility = VISIBILITY_SEMI_TRANSPARENT; + if (front == OPAQUE || behind == OPAQUE || bubble == OPAQUE) { + visibility = OPAQUE; + } else if (front > TRANSPARENT || behind > TRANSPARENT || bubble > TRANSPARENT) { + visibility = SEMI_TRANSPARENT; } else { - visibility = VISIBILITY_FULLY_TRANSPARENT; + visibility = TRANSPARENT; } - Assert.assertEquals("Invalid visibility.", visibility, mScrimVisibility); + Assert.assertEquals("Invalid visibility.", + visibility /* expected */, + mScrimVisibility); } /** @@ -681,11 +785,12 @@ public class ScrimControllerTest extends SysuiTestCase { boolean mOnPreDrawCalled; SynchronousScrimController(ScrimView scrimBehind, ScrimView scrimInFront, + ScrimView scrimForBubble, TriConsumer<ScrimState, Float, GradientColors> scrimStateListener, Consumer<Integer> scrimVisibleListener, DozeParameters dozeParameters, AlarmManager alarmManager, KeyguardMonitor keyguardMonitor) { - super(scrimBehind, scrimInFront, scrimStateListener, scrimVisibleListener, - dozeParameters, alarmManager, keyguardMonitor); + super(scrimBehind, scrimInFront, scrimForBubble, scrimStateListener, + scrimVisibleListener, dozeParameters, alarmManager, keyguardMonitor); } @Override @@ -696,13 +801,14 @@ public class ScrimControllerTest extends SysuiTestCase { void finishAnimationsImmediately() { boolean[] animationFinished = {false}; - setOnAnimationFinished(()-> animationFinished[0] = true); + setOnAnimationFinished(() -> animationFinished[0] = true); // Execute code that will trigger animations. onPreDraw(); // Force finish all animations. mLooper.processAllMessages(); endAnimation(mScrimBehind, TAG_KEY_ANIM); endAnimation(mScrimInFront, TAG_KEY_ANIM); + endAnimation(mScrimForBubble, TAG_KEY_ANIM); if (!animationFinished[0]) { throw new IllegalStateException("Animation never finished"); @@ -740,6 +846,7 @@ public class ScrimControllerTest extends SysuiTestCase { /** * Do not wait for a frame since we're in a test environment. + * * @param callback What to execute. */ @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index 06d76ebcff28..5a6f27dcfaae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -153,8 +153,6 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { StatusBarNotification bubbleSbn = mBubbleNotificationRow.getStatusBarNotification(); bubbleSbn.getNotification().contentIntent = mContentIntent; bubbleSbn.getNotification().flags |= Notification.FLAG_AUTO_CANCEL; - // Do what BubbleController's NotificationEntryListener#onPendingEntryAdded does: - mBubbleNotificationRow.getEntry().setShowInShadeWhenBubble(true); mActiveNotifications = new ArrayList<>(); mActiveNotifications.add(mNotificationRow.getEntry()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java index 7fbf18367f61..a1afd1d9de29 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java @@ -208,7 +208,8 @@ public class StatusBarTest extends SysuiTestCase { mNotificationInterruptionStateProvider = new TestableNotificationInterruptionStateProvider(mContext, mPowerManager, - mDreamManager, mAmbientDisplayConfiguration); + mDreamManager, mAmbientDisplayConfiguration, mNotificationFilter, + mStatusBarStateController); mDependency.injectTestDependency(NotificationInterruptionStateProvider.class, mNotificationInterruptionStateProvider); mDependency.injectMockDependency(NavigationBarController.class); @@ -870,8 +871,11 @@ public class StatusBarTest extends SysuiTestCase { Context context, PowerManager powerManager, IDreamManager dreamManager, - AmbientDisplayConfiguration ambientDisplayConfiguration) { - super(context, powerManager, dreamManager, ambientDisplayConfiguration); + AmbientDisplayConfiguration ambientDisplayConfiguration, + NotificationFilter filter, + StatusBarStateController controller) { + super(context, powerManager, dreamManager, ambientDisplayConfiguration, filter, + controller); mUseHeadsUp = true; } } diff --git a/services/core/java/com/android/server/notification/BubbleExtractor.java b/services/core/java/com/android/server/notification/BubbleExtractor.java index 358bdb90f6d3..e59bf1642ba8 100644 --- a/services/core/java/com/android/server/notification/BubbleExtractor.java +++ b/services/core/java/com/android/server/notification/BubbleExtractor.java @@ -41,10 +41,9 @@ public class BubbleExtractor implements NotificationSignalExtractor { if (DBG) Slog.d(TAG, "missing config"); return null; } - boolean userWantsBubbles = mConfig.bubblesEnabled(record.sbn.getUser()); boolean appCanShowBubble = mConfig.areBubblesAllowed(record.sbn.getPackageName(), record.sbn.getUid()); - if (!userWantsBubbles || !appCanShowBubble) { + if (!mConfig.bubblesEnabled() || !appCanShowBubble) { record.setAllowBubble(false); } else { if (record.getChannel() != null) { diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 30b2245ec24e..f04d4590f6ad 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -94,6 +94,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER; +import static com.android.server.notification.PreferencesHelper.DEFAULT_ALLOW_BUBBLE; import static com.android.server.utils.PriorityDump.PRIORITY_ARG; import static com.android.server.utils.PriorityDump.PRIORITY_ARG_CRITICAL; import static com.android.server.utils.PriorityDump.PRIORITY_ARG_NORMAL; @@ -1391,7 +1392,9 @@ public class NotificationManagerService extends SystemService { private final class SettingsObserver extends ContentObserver { private final Uri NOTIFICATION_BADGING_URI = Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_BADGING); - private final Uri NOTIFICATION_BUBBLES_URI + private final Uri NOTIFICATION_BUBBLES_URI_GLOBAL + = Settings.Global.getUriFor(Settings.Global.NOTIFICATION_BUBBLES); + private final Uri NOTIFICATION_BUBBLES_URI_SECURE = Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_BUBBLES); private final Uri NOTIFICATION_LIGHT_PULSE_URI = Settings.System.getUriFor(Settings.System.NOTIFICATION_LIGHT_PULSE); @@ -1410,7 +1413,9 @@ public class NotificationManagerService extends SystemService { false, this, UserHandle.USER_ALL); resolver.registerContentObserver(NOTIFICATION_RATE_LIMIT_URI, false, this, UserHandle.USER_ALL); - resolver.registerContentObserver(NOTIFICATION_BUBBLES_URI, + resolver.registerContentObserver(NOTIFICATION_BUBBLES_URI_GLOBAL, + false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(NOTIFICATION_BUBBLES_URI_SECURE, false, this, UserHandle.USER_ALL); update(null); } @@ -1437,9 +1442,41 @@ public class NotificationManagerService extends SystemService { if (uri == null || NOTIFICATION_BADGING_URI.equals(uri)) { mPreferencesHelper.updateBadgingEnabled(); } - if (uri == null || NOTIFICATION_BUBBLES_URI.equals(uri)) { + // In QPR we moved the setting to Global rather than Secure so that the setting + // applied to work profiles. Unfortunately we need to maintain both to pass CTS without + // a change to CTS outside of a normal letter release. + if (uri == null || NOTIFICATION_BUBBLES_URI_GLOBAL.equals(uri)) { + syncBubbleSettings(resolver, NOTIFICATION_BUBBLES_URI_GLOBAL); mPreferencesHelper.updateBubblesEnabled(); } + if (NOTIFICATION_BUBBLES_URI_SECURE.equals(uri)) { + syncBubbleSettings(resolver, NOTIFICATION_BUBBLES_URI_SECURE); + } + } + + private void syncBubbleSettings(ContentResolver resolver, Uri settingToFollow) { + boolean followSecureSetting = settingToFollow.equals(NOTIFICATION_BUBBLES_URI_SECURE); + + int secureSettingValue = Settings.Secure.getInt(resolver, + Settings.Secure.NOTIFICATION_BUBBLES, DEFAULT_ALLOW_BUBBLE ? 1 : 0); + int globalSettingValue = Settings.Global.getInt(resolver, + Settings.Global.NOTIFICATION_BUBBLES, DEFAULT_ALLOW_BUBBLE ? 1 : 0); + + if (globalSettingValue == secureSettingValue) { + return; + } + + if (followSecureSetting) { + // Global => secure + Settings.Global.putInt(resolver, + Settings.Global.NOTIFICATION_BUBBLES, + secureSettingValue); + } else { + // Secure => Global + Settings.Secure.putInt(resolver, + Settings.Secure.NOTIFICATION_BADGING, + globalSettingValue); + } } } @@ -4958,47 +4995,112 @@ public class NotificationManagerService extends SystemService { } else { notification.flags &= ~FLAG_BUBBLE; } + // Is the app in the foreground? + final boolean appIsForeground = + mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND; + Notification.BubbleMetadata metadata = notification.getBubbleMetadata(); + if (!appIsForeground && metadata != null) { + // Remove any flags that only work when foregrounded + int flags = metadata.getFlags(); + flags &= ~Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; + flags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; + metadata.setFlags(flags); + } } /** - * @return whether the provided notification record is allowed to be represented as a bubble. + * @return whether the provided notification record is allowed to be represented as a bubble, + * accounting for user choice & policy. */ private boolean isNotificationAppropriateToBubble(NotificationRecord r, String pkg, int userId, NotificationRecord oldRecord) { Notification notification = r.getNotification(); - Notification.BubbleMetadata metadata = notification.getBubbleMetadata(); - boolean intentCanBubble = metadata != null - && canLaunchInActivityView(getContext(), metadata.getIntent(), pkg); + if (!canBubble(r, pkg, userId)) { + // no log: canBubble has its own + return false; + } - // Does the app want to bubble & is able to bubble - boolean canBubble = intentCanBubble - && mPreferencesHelper.areBubblesAllowed(pkg, userId) - && mPreferencesHelper.bubblesEnabled(r.sbn.getUser()) - && r.getChannel().canBubble() - && !mActivityManager.isLowRamDevice(); + if (mActivityManager.isLowRamDevice()) { + logBubbleError(r.getKey(), "low ram device"); + return false; + } - // Is the app in the foreground? - final boolean appIsForeground = - mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND; + if (mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND) { + // If the app is foreground it always gets to bubble + return true; + } + + if (oldRecord != null && (oldRecord.getNotification().flags & FLAG_BUBBLE) != 0) { + // This is an update to an active bubble + return true; + } + + // At this point the bubble must fulfill communication policy - // Is the notification something we'd allow to bubble? - // A call with a foreground service + person + // Communication always needs a person ArrayList<Person> peopleList = notification.extras != null ? notification.extras.getParcelableArrayList(Notification.EXTRA_PEOPLE_LIST) : null; - boolean isForegroundCall = CATEGORY_CALL.equals(notification.category) - && (notification.flags & FLAG_FOREGROUND_SERVICE) != 0; - // OR message style (which always has a person) with any remote input - Class<? extends Notification.Style> style = notification.getNotificationStyle(); - boolean isMessageStyle = Notification.MessagingStyle.class.equals(style); - boolean notificationAppropriateToBubble = - (isMessageStyle && hasValidRemoteInput(notification)) - || (peopleList != null && !peopleList.isEmpty() && isForegroundCall); + // Message style requires a person & it's not included in the list + boolean isMessageStyle = Notification.MessagingStyle.class.equals( + notification.getNotificationStyle()); + if (!isMessageStyle && (peopleList == null || peopleList.isEmpty())) { + logBubbleError(r.getKey(), "if not foreground, must have a person and be " + + "Notification.MessageStyle or Notification.CATEGORY_CALL"); + return false; + } - // OR something that was previously a bubble & still exists - boolean bubbleUpdate = oldRecord != null - && (oldRecord.getNotification().flags & FLAG_BUBBLE) != 0; - return canBubble && (notificationAppropriateToBubble || appIsForeground || bubbleUpdate); + // Communication is a message or a call + boolean isCall = CATEGORY_CALL.equals(notification.category); + boolean hasForegroundService = (notification.flags & FLAG_FOREGROUND_SERVICE) != 0; + if (isMessageStyle) { + if (hasValidRemoteInput(notification)) { + return true; + } + logBubbleError(r.getKey(), "messages require valid remote input"); + return false; + } else if (isCall) { + if (hasForegroundService) { + return true; + } + logBubbleError(r.getKey(), "calls require foreground service"); + return false; + } + logBubbleError(r.getKey(), "if not foreground, must be " + + "Notification.MessageStyle or Notification.CATEGORY_CALL"); + return false; + } + + /** + * @return whether the user has enabled the provided notification to bubble, does not account + * for policy. + */ + private boolean canBubble(NotificationRecord r, String pkg, int userId) { + Notification notification = r.getNotification(); + Notification.BubbleMetadata metadata = notification.getBubbleMetadata(); + if (metadata == null) { + // no log: no need to inform dev if they didn't attach bubble metadata + return false; + } + if (!canLaunchInActivityView(getContext(), metadata.getIntent(), pkg)) { + // no log: method has the failure log + return false; + } + if (!mPreferencesHelper.bubblesEnabled()) { + logBubbleError(r.getKey(), "bubbles disabled for user: " + userId); + return false; + } + if (!mPreferencesHelper.areBubblesAllowed(pkg, userId)) { + logBubbleError(r.getKey(), + "bubbles for package: " + pkg + " disabled for user: " + userId); + return false; + } + if (!r.getChannel().canBubble()) { + logBubbleError(r.getKey(), + "bubbles for channel " + r.getChannel().getId() + " disabled"); + return false; + } + return true; } private boolean hasValidRemoteInput(Notification n) { @@ -5017,6 +5119,11 @@ public class NotificationManagerService extends SystemService { return false; } + private void logBubbleError(String key, String failureMessage) { + if (DBG) { + Log.w(TAG, "Bubble notification: " + key + " failed: " + failureMessage); + } + } /** * Whether an intent is properly configured to display in an {@link android.app.ActivityView}. * @@ -5383,12 +5490,26 @@ public class NotificationManagerService extends SystemService { return; } + // Bubbled children get to stick around if the summary was manually cancelled + // (user removed) from systemui. + FlagChecker childrenFlagChecker = null; + if (mReason == REASON_CANCEL + || mReason == REASON_CLICK + || mReason == REASON_CANCEL_ALL) { + childrenFlagChecker = (flags) -> { + if ((flags & FLAG_BUBBLE) != 0) { + return false; + } + return true; + }; + } + // Cancel the notification. boolean wasPosted = removeFromNotificationListsLocked(r); cancelNotificationLocked( r, mSendDelete, mReason, mRank, mCount, wasPosted, listenerName); cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName, - mSendDelete, null); + mSendDelete, childrenFlagChecker); updateLightsLocked(); } else { // No notification was found, assume that it is snoozed and cancel it. diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index d580bd6f0a11..082b08debe9f 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -104,7 +104,7 @@ public class PreferencesHelper implements RankingConfig { @VisibleForTesting static final boolean DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS = false; private static final boolean DEFAULT_SHOW_BADGE = true; - private static final boolean DEFAULT_ALLOW_BUBBLE = true; + static final boolean DEFAULT_ALLOW_BUBBLE = true; private static final boolean DEFAULT_OEM_LOCKED_IMPORTANCE = false; private static final boolean DEFAULT_APP_LOCKED_IMPORTANCE = false; @@ -134,7 +134,7 @@ public class PreferencesHelper implements RankingConfig { private final ZenModeHelper mZenModeHelper; private SparseBooleanArray mBadgingEnabled; - private SparseBooleanArray mBubblesEnabled; + private boolean mBubblesEnabled = DEFAULT_ALLOW_BUBBLE; private boolean mAreChannelsBypassingDnd; private boolean mHideSilentStatusBarIcons = DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS; @@ -1840,40 +1840,19 @@ public class PreferencesHelper implements RankingConfig { } public void updateBubblesEnabled() { - if (mBubblesEnabled == null) { - mBubblesEnabled = new SparseBooleanArray(); - } - boolean changed = false; - // update the cached values - for (int index = 0; index < mBubblesEnabled.size(); index++) { - int userId = mBubblesEnabled.keyAt(index); - final boolean oldValue = mBubblesEnabled.get(userId); - final boolean newValue = Settings.Secure.getIntForUser(mContext.getContentResolver(), - Settings.Secure.NOTIFICATION_BUBBLES, - DEFAULT_ALLOW_BUBBLE ? 1 : 0, userId) != 0; - mBubblesEnabled.put(userId, newValue); - changed |= oldValue != newValue; - } - if (changed) { + final boolean newValue = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.NOTIFICATION_BUBBLES, + DEFAULT_ALLOW_BUBBLE ? 1 : 0) == 1; + if (newValue != mBubblesEnabled) { + mBubblesEnabled = newValue; updateConfig(); } } - public boolean bubblesEnabled(UserHandle userHandle) { - int userId = userHandle.getIdentifier(); - if (userId == UserHandle.USER_ALL) { - return false; - } - if (mBubblesEnabled.indexOfKey(userId) < 0) { - mBubblesEnabled.put(userId, - Settings.Secure.getIntForUser(mContext.getContentResolver(), - Settings.Secure.NOTIFICATION_BUBBLES, - DEFAULT_ALLOW_BUBBLE ? 1 : 0, userId) != 0); - } - return mBubblesEnabled.get(userId, DEFAULT_ALLOW_BUBBLE); + public boolean bubblesEnabled() { + return mBubblesEnabled; } - public void updateBadgingEnabled() { if (mBadgingEnabled == null) { mBadgingEnabled = new SparseBooleanArray(); diff --git a/services/core/java/com/android/server/notification/RankingConfig.java b/services/core/java/com/android/server/notification/RankingConfig.java index 5de00e43a05d..7816f3619023 100644 --- a/services/core/java/com/android/server/notification/RankingConfig.java +++ b/services/core/java/com/android/server/notification/RankingConfig.java @@ -30,7 +30,7 @@ public interface RankingConfig { boolean canShowBadge(String packageName, int uid); boolean badgingEnabled(UserHandle userHandle); boolean areBubblesAllowed(String packageName, int uid); - boolean bubblesEnabled(UserHandle userHandle); + boolean bubblesEnabled(); boolean isGroupBlocked(String packageName, int uid, String groupId); Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg, diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java index 5bd557df0616..b17036a2f9a2 100644 --- a/services/core/java/com/android/server/wm/ActivityStack.java +++ b/services/core/java/com/android/server/wm/ActivityStack.java @@ -4758,6 +4758,7 @@ class ActivityStack extends ConfigurationContainer { task.cleanUpResourcesForDestroy(); } + final ActivityDisplay display = getDisplay(); if (mTaskHistory.isEmpty()) { if (DEBUG_STACK) Slog.i(TAG_STACK, "removeTask: removing stack=" + this); // We only need to adjust focused stack if this stack is in focus and we are not in the @@ -4766,11 +4767,11 @@ class ActivityStack extends ConfigurationContainer { && mRootActivityContainer.isTopDisplayFocusedStack(this)) { String myReason = reason + " leftTaskHistoryEmpty"; if (!inMultiWindowMode() || adjustFocusToNextFocusableStack(myReason) == null) { - getDisplay().moveHomeStackToFront(myReason); + display.moveHomeStackToFront(myReason); } } if (isAttached()) { - getDisplay().positionChildAtBottom(this); + display.positionChildAtBottom(this); } if (!isActivityTypeHome() || !isAttached()) { remove(); @@ -4783,6 +4784,9 @@ class ActivityStack extends ConfigurationContainer { if (inPinnedWindowingMode()) { mService.getTaskChangeNotificationController().notifyActivityUnpinned(); } + if (display.isSingleTaskInstance()) { + mService.notifySingleTaskDisplayEmpty(display.mDisplayId); + } } TaskRecord createTaskRecord(int taskId, ActivityInfo info, Intent intent, diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index f2ca2ba82dfb..1bcf47b42c97 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -5923,6 +5923,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return allUids.contains(uid); } + void notifySingleTaskDisplayEmpty(int displayId) { + mTaskChangeNotificationController.notifySingleTaskDisplayEmpty(displayId); + } + final class H extends Handler { static final int REPORT_TIME_TRACKER_MSG = 1; diff --git a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java index c2c476741963..5e8831d47c12 100644 --- a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java +++ b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java @@ -57,6 +57,7 @@ class TaskChangeNotificationController { private static final int NOTIFY_SINGLE_TASK_DISPLAY_DRAWN = 22; private static final int NOTIFY_TASK_DISPLAY_CHANGED_LISTENERS_MSG = 23; private static final int NOTIFY_TASK_LIST_UPDATED_LISTENERS_MSG = 24; + private static final int NOTIFY_SINGLE_TASK_DISPLAY_EMPTY = 25; // Delay in notifying task stack change listeners (in millis) private static final int NOTIFY_TASK_STACK_CHANGE_LISTENERS_DELAY = 100; @@ -161,6 +162,10 @@ class TaskChangeNotificationController { l.onSingleTaskDisplayDrawn(m.arg1); }; + private final TaskStackConsumer mNotifySingleTaskDisplayEmpty = (l, m) -> { + l.onSingleTaskDisplayEmpty(m.arg1); + }; + private final TaskStackConsumer mNotifyTaskDisplayChanged = (l, m) -> { l.onTaskDisplayChanged(m.arg1, m.arg2); }; @@ -251,6 +256,9 @@ class TaskChangeNotificationController { case NOTIFY_SINGLE_TASK_DISPLAY_DRAWN: forAllRemoteListeners(mNotifySingleTaskDisplayDrawn, msg); break; + case NOTIFY_SINGLE_TASK_DISPLAY_EMPTY: + forAllRemoteListeners(mNotifySingleTaskDisplayEmpty, msg); + break; case NOTIFY_TASK_DISPLAY_CHANGED_LISTENERS_MSG: forAllRemoteListeners(mNotifyTaskDisplayChanged, msg); break; @@ -513,6 +521,17 @@ class TaskChangeNotificationController { } /** + * Notify listeners that the last task is removed from a single task display. + */ + void notifySingleTaskDisplayEmpty(int displayId) { + final Message msg = mHandler.obtainMessage( + NOTIFY_SINGLE_TASK_DISPLAY_EMPTY, + displayId, 0 /* unused */); + forAllLocalListeners(mNotifySingleTaskDisplayEmpty, msg); + msg.sendToTarget(); + } + + /** * Notify listeners that a task is reparented to another display. */ void notifyTaskDisplayChanged(int taskId, int newDisplayId) { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BubbleExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BubbleExtractorTest.java index 273a9e66e55b..7459c4b0610e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/BubbleExtractorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/BubbleExtractorTest.java @@ -86,7 +86,7 @@ public class BubbleExtractorTest extends UiServiceTestCase { BubbleExtractor extractor = new BubbleExtractor(); extractor.setConfig(mConfig); - when(mConfig.bubblesEnabled(mUser)).thenReturn(true); + when(mConfig.bubblesEnabled()).thenReturn(true); when(mConfig.areBubblesAllowed(mPkg, mUid)).thenReturn(true); NotificationRecord r = getNotificationRecord(false, IMPORTANCE_UNSPECIFIED); @@ -100,7 +100,7 @@ public class BubbleExtractorTest extends UiServiceTestCase { BubbleExtractor extractor = new BubbleExtractor(); extractor.setConfig(mConfig); - when(mConfig.bubblesEnabled(mUser)).thenReturn(true); + when(mConfig.bubblesEnabled()).thenReturn(true); when(mConfig.areBubblesAllowed(mPkg, mUid)).thenReturn(false); NotificationRecord r = getNotificationRecord(true, IMPORTANCE_HIGH); @@ -114,7 +114,7 @@ public class BubbleExtractorTest extends UiServiceTestCase { BubbleExtractor extractor = new BubbleExtractor(); extractor.setConfig(mConfig); - when(mConfig.bubblesEnabled(mUser)).thenReturn(true); + when(mConfig.bubblesEnabled()).thenReturn(true); when(mConfig.areBubblesAllowed(mPkg, mUid)).thenReturn(true); NotificationRecord r = getNotificationRecord(true, IMPORTANCE_UNSPECIFIED); @@ -128,7 +128,7 @@ public class BubbleExtractorTest extends UiServiceTestCase { BubbleExtractor extractor = new BubbleExtractor(); extractor.setConfig(mConfig); - when(mConfig.bubblesEnabled(mUser)).thenReturn(true); + when(mConfig.bubblesEnabled()).thenReturn(true); when(mConfig.areBubblesAllowed(mPkg, mUid)).thenReturn(false); NotificationRecord r = getNotificationRecord(false, IMPORTANCE_UNSPECIFIED); @@ -142,7 +142,7 @@ public class BubbleExtractorTest extends UiServiceTestCase { BubbleExtractor extractor = new BubbleExtractor(); extractor.setConfig(mConfig); - when(mConfig.bubblesEnabled(mUser)).thenReturn(false); + when(mConfig.bubblesEnabled()).thenReturn(false); when(mConfig.areBubblesAllowed(mPkg, mUid)).thenReturn(true); NotificationRecord r = getNotificationRecord(true, IMPORTANCE_HIGH); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 3ac7a79a1630..be638a9d9755 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -20,6 +20,7 @@ import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREG import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; import static android.app.Notification.CATEGORY_CALL; +import static android.app.Notification.FLAG_AUTO_CANCEL; import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_FOREGROUND_SERVICE; import static android.app.NotificationManager.EXTRA_BLOCKED_STATE; @@ -446,7 +447,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private void setUpPrefsForBubbles(boolean globalEnabled, boolean pkgEnabled, boolean channelEnabled) { mService.setPreferencesHelper(mPreferencesHelper); - when(mPreferencesHelper.bubblesEnabled(any())).thenReturn(globalEnabled); + when(mPreferencesHelper.bubblesEnabled()).thenReturn(globalEnabled); when(mPreferencesHelper.areBubblesAllowed(anyString(), anyInt())).thenReturn(pkgEnabled); when(mPreferencesHelper.getNotificationChannel( anyString(), anyInt(), anyString(), anyBoolean())).thenReturn( @@ -465,14 +466,22 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { return sbn; } + private NotificationRecord generateNotificationRecord(NotificationChannel channel, int id, String groupKey, boolean isSummary) { + return generateNotificationRecord(channel, id, groupKey, isSummary, false /* isBubble */); + } + + private NotificationRecord generateNotificationRecord(NotificationChannel channel, int id, + String groupKey, boolean isSummary, boolean isBubble) { Notification.Builder nb = new Notification.Builder(mContext, channel.getId()) .setContentTitle("foo") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setGroup(groupKey) .setGroupSummary(isSummary); - + if (isBubble) { + nb.setBubbleMetadata(getBasicBubbleMetadataBuilder().build()); + } StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, id, "tag", mUid, 0, nb.build(), new UserHandle(mUid), null, 0); return new NotificationRecord(mContext, sbn, channel); @@ -568,6 +577,52 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .setIcon(Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon)); } + private NotificationRecord addGroupWithBubblesAndValidateAdded(boolean summaryAutoCancel) + throws RemoteException { + + // Notification that has bubble metadata + NotificationRecord nrBubble = generateNotificationRecord(mTestNotificationChannel, 1, + "BUBBLE_GROUP", false /* isSummary */, true /* isBubble */); + + // Make the package foreground so that we're allowed to be a bubble + when(mActivityManager.getPackageImportance(nrBubble.sbn.getPackageName())).thenReturn( + IMPORTANCE_FOREGROUND); + + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", + nrBubble.sbn.getId(), nrBubble.sbn.getNotification(), nrBubble.sbn.getUserId()); + waitForIdle(); + + // Make sure we are a bubble + StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG); + assertEquals(1, notifsAfter.length); + assertTrue((notifsAfter[0].getNotification().flags & FLAG_BUBBLE) != 0); + + // Plain notification without bubble metadata + NotificationRecord nrPlain = generateNotificationRecord(mTestNotificationChannel, 2, + "BUBBLE_GROUP", false /* isSummary */, false /* isBubble */); + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", + nrPlain.sbn.getId(), nrPlain.sbn.getNotification(), nrPlain.sbn.getUserId()); + waitForIdle(); + + notifsAfter = mBinderService.getActiveNotifications(PKG); + assertEquals(2, notifsAfter.length); + + // Summary notification for both of those + NotificationRecord nrSummary = generateNotificationRecord(mTestNotificationChannel, 3, + "BUBBLE_GROUP", true /* isSummary */, false /* isBubble */); + if (summaryAutoCancel) { + nrSummary.getNotification().flags |= FLAG_AUTO_CANCEL; + } + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", + nrSummary.sbn.getId(), nrSummary.sbn.getNotification(), nrSummary.sbn.getUserId()); + waitForIdle(); + + notifsAfter = mBinderService.getActiveNotifications(PKG); + assertEquals(3, notifsAfter.length); + + return nrSummary; + } + @Test public void testCreateNotificationChannels_SingleChannel() throws Exception { final NotificationChannel channel = @@ -5318,4 +5373,161 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mUsageStats, times(5)).registerImageRemoved(PKG); } + + public void testNotificationBubbles_flagAutoExpandForeground_fails_notForeground() + throws Exception { + // Bubbles are allowed! + setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */); + + // Give it bubble metadata + Notification.BubbleMetadata data = getBasicBubbleMetadataBuilder() + .setSuppressNotification(true) + .setAutoExpandBubble(true).build(); + // Give it a person + Person person = new Person.Builder() + .setName("bubblebot") + .build(); + // It needs remote input to be bubble-able + RemoteInput remoteInput = new RemoteInput.Builder("reply_key").setLabel("reply").build(); + PendingIntent inputIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0); + Icon icon = Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon); + Notification.Action replyAction = new Notification.Action.Builder(icon, "Reply", + inputIntent).addRemoteInput(remoteInput) + .build(); + // Make it messaging style + Notification.Builder nb = new Notification.Builder(mContext, + mTestNotificationChannel.getId()) + .setContentTitle("foo") + .setBubbleMetadata(data) + .setStyle(new Notification.MessagingStyle(person) + .setConversationTitle("Bubble Chat") + .addMessage("Hello?", + SystemClock.currentThreadTimeMillis() - 300000, person) + .addMessage("Is it me you're looking for?", + SystemClock.currentThreadTimeMillis(), person) + ) + .setActions(replyAction) + .setSmallIcon(android.R.drawable.sym_def_app_icon); + + StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, null, mUid, 0, + nb.build(), new UserHandle(mUid), null, 0); + NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); + + // Ensure we're not foreground + when(mActivityManager.getPackageImportance(nr.sbn.getPackageName())).thenReturn( + IMPORTANCE_VISIBLE); + + mBinderService.enqueueNotificationWithTag(PKG, PKG, null, + nr.sbn.getId(), nr.sbn.getNotification(), nr.sbn.getUserId()); + waitForIdle(); + + // yes allowed, yes messaging, yes bubble + Notification notif = mService.getNotificationRecord(sbn.getKey()).getNotification(); + assertTrue(notif.isBubbleNotification()); + + // Our flags should have failed since we're not foreground + assertFalse(notif.getBubbleMetadata().getAutoExpandBubble()); + assertFalse(notif.getBubbleMetadata().isNotificationSuppressed()); + } + + @Test + public void testNotificationBubbles_flagAutoExpandForeground_succeeds_foreground() + throws RemoteException { + // Bubbles are allowed! + setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */); + + // Give it bubble metadata + Notification.BubbleMetadata data = getBasicBubbleMetadataBuilder() + .setSuppressNotification(true) + .setAutoExpandBubble(true).build(); + // Give it a person + Person person = new Person.Builder() + .setName("bubblebot") + .build(); + // It needs remote input to be bubble-able + RemoteInput remoteInput = new RemoteInput.Builder("reply_key").setLabel("reply").build(); + PendingIntent inputIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0); + Icon icon = Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon); + Notification.Action replyAction = new Notification.Action.Builder(icon, "Reply", + inputIntent).addRemoteInput(remoteInput) + .build(); + // Make it messaging style + Notification.Builder nb = new Notification.Builder(mContext, + mTestNotificationChannel.getId()) + .setContentTitle("foo") + .setBubbleMetadata(data) + .setStyle(new Notification.MessagingStyle(person) + .setConversationTitle("Bubble Chat") + .addMessage("Hello?", + SystemClock.currentThreadTimeMillis() - 300000, person) + .addMessage("Is it me you're looking for?", + SystemClock.currentThreadTimeMillis(), person) + ) + .setActions(replyAction) + .setSmallIcon(android.R.drawable.sym_def_app_icon); + + StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, null, mUid, 0, + nb.build(), new UserHandle(mUid), null, 0); + NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); + + // Ensure we are in the foreground + when(mActivityManager.getPackageImportance(nr.sbn.getPackageName())).thenReturn( + IMPORTANCE_FOREGROUND); + + mBinderService.enqueueNotificationWithTag(PKG, PKG, null, + nr.sbn.getId(), nr.sbn.getNotification(), nr.sbn.getUserId()); + waitForIdle(); + + // yes allowed, yes messaging, yes bubble + Notification notif = mService.getNotificationRecord(sbn.getKey()).getNotification(); + assertTrue(notif.isBubbleNotification()); + + // Our flags should have failed since we are foreground + assertTrue(notif.getBubbleMetadata().getAutoExpandBubble()); + assertTrue(notif.getBubbleMetadata().isNotificationSuppressed()); + } + + @Test + public void testNotificationBubbles_bubbleChildrenStay_whenGroupSummaryDismissed() + throws Exception { + // Bubbles are allowed! + setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */); + + NotificationRecord nrSummary = addGroupWithBubblesAndValidateAdded( + true /* summaryAutoCancel */); + + // Dismiss summary + final NotificationVisibility nv = NotificationVisibility.obtain(nrSummary.getKey(), 1, 2, + true); + mService.mNotificationDelegate.onNotificationClear(mUid, 0, PKG, nrSummary.sbn.getTag(), + nrSummary.sbn.getId(), nrSummary.getUserId(), nrSummary.getKey(), + NotificationStats.DISMISSAL_SHADE, + NotificationStats.DISMISS_SENTIMENT_NEUTRAL, nv); + waitForIdle(); + + // The bubble should still exist + StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG); + assertEquals(1, notifsAfter.length); + } + + @Test + public void testNotificationBubbles_bubbleChildrenStay_whenGroupSummaryClicked() + throws Exception { + // Bubbles are allowed! + setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */); + + NotificationRecord nrSummary = addGroupWithBubblesAndValidateAdded( + true /* summaryAutoCancel */); + + // Click summary + final NotificationVisibility nv = NotificationVisibility.obtain(nrSummary.getKey(), 1, 2, + true); + mService.mNotificationDelegate.onNotificationClick(mUid, Binder.getCallingPid(), + nrSummary.getKey(), nv); + waitForIdle(); + + // The bubble should still exist + StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG); + assertEquals(1, notifsAfter.length); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 365cd80c88c7..80439cf66387 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -60,7 +60,9 @@ import android.net.Uri; import android.os.Build; import android.os.UserHandle; import android.provider.Settings; +import android.provider.Settings.Global; import android.provider.Settings.Secure; + import android.test.suitebuilder.annotation.SmallTest; import android.testing.TestableContentResolver; import android.util.ArrayMap; @@ -154,8 +156,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { contentResolver.setFallbackToExisting(false); Secure.putIntForUser(contentResolver, Secure.NOTIFICATION_BADGING, 1, UserHandle.getUserId(UID_N_MR1)); - Secure.putIntForUser(contentResolver, - Secure.NOTIFICATION_BUBBLES, 1, UserHandle.getUserId(UID_N_MR1)); + Global.putInt(contentResolver, Global.NOTIFICATION_BUBBLES, 1); ContentProvider testContentProvider = mock(ContentProvider.class); when(testContentProvider.getIContentProvider()).thenReturn(mTestIContentProvider); @@ -1950,42 +1951,18 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testBubblesOverrideTrue() { - Secure.putIntForUser(getContext().getContentResolver(), - Secure.NOTIFICATION_BUBBLES, 1, - USER.getIdentifier()); + Global.putInt(getContext().getContentResolver(), + Global.NOTIFICATION_BUBBLES, 1); mHelper.updateBubblesEnabled(); // would be called by settings observer - assertTrue(mHelper.bubblesEnabled(USER)); + assertTrue(mHelper.bubblesEnabled()); } @Test public void testBubblesOverrideFalse() { - Secure.putIntForUser(getContext().getContentResolver(), - Secure.NOTIFICATION_BUBBLES, 0, - USER.getIdentifier()); - mHelper.updateBubblesEnabled(); // would be called by settings observer - assertFalse(mHelper.bubblesEnabled(USER)); - } - - @Test - public void testBubblesForUserAll() { - try { - mHelper.bubblesEnabled(UserHandle.ALL); - } catch (Exception e) { - fail("just don't throw"); - } - } - - @Test - public void testBubblesOverrideUserIsolation() { - Secure.putIntForUser(getContext().getContentResolver(), - Secure.NOTIFICATION_BUBBLES, 0, - USER.getIdentifier()); - Secure.putIntForUser(getContext().getContentResolver(), - Secure.NOTIFICATION_BUBBLES, 1, - USER2.getIdentifier()); + Global.putInt(getContext().getContentResolver(), + Global.NOTIFICATION_BUBBLES, 0); mHelper.updateBubblesEnabled(); // would be called by settings observer - assertFalse(mHelper.bubblesEnabled(USER)); - assertTrue(mHelper.bubblesEnabled(USER2)); + assertFalse(mHelper.bubblesEnabled()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java index 19fd93fee5f0..6e41118997ac 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java @@ -271,6 +271,54 @@ public class TaskStackChangedListenerTest { waitForCallback(singleTaskDisplayDrawnLatch); } + @Test + public void testSingleTaskDisplayEmpty() throws Exception { + final Instrumentation instrumentation = getInstrumentation(); + + final CountDownLatch activityViewReadyLatch = new CountDownLatch(1); + final CountDownLatch activityViewDestroyedLatch = new CountDownLatch(1); + final CountDownLatch singleTaskDisplayDrawnLatch = new CountDownLatch(1); + final CountDownLatch singleTaskDisplayEmptyLatch = new CountDownLatch(1); + + registerTaskStackChangedListener(new TaskStackListener() { + @Override + public void onSingleTaskDisplayDrawn(int displayId) throws RemoteException { + singleTaskDisplayDrawnLatch.countDown(); + } + @Override + public void onSingleTaskDisplayEmpty(int displayId) + throws RemoteException { + singleTaskDisplayEmptyLatch.countDown(); + } + }); + final ActivityViewTestActivity activity = + (ActivityViewTestActivity) startTestActivity(ActivityViewTestActivity.class); + final ActivityView activityView = activity.getActivityView(); + activityView.setCallback(new ActivityView.StateCallback() { + @Override + public void onActivityViewReady(ActivityView view) { + activityViewReadyLatch.countDown(); + } + + @Override + public void onActivityViewDestroyed(ActivityView view) { + activityViewDestroyedLatch.countDown(); + } + }); + waitForCallback(activityViewReadyLatch); + + final Context context = instrumentation.getContext(); + Intent intent = new Intent(context, ActivityInActivityView.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + activityView.startActivity(intent); + waitForCallback(singleTaskDisplayDrawnLatch); + assertEquals(1, singleTaskDisplayEmptyLatch.getCount()); + + activityView.release(); + waitForCallback(activityViewDestroyedLatch); + waitForCallback(singleTaskDisplayEmptyLatch); + } + /** * Starts the provided activity and returns the started instance. */ |