diff options
97 files changed, 4269 insertions, 819 deletions
diff --git a/Android.bp b/Android.bp index 501b43863..f46c206d4 100644 --- a/Android.bp +++ b/Android.bp @@ -28,6 +28,7 @@ android_app { static_libs: [ "androidx.annotation_annotation", "androidx.core_core", + "telecom_flags_core_java_lib", ], libs: [ "services", @@ -50,6 +51,7 @@ android_test { name: "TelecomUnitTests", static_libs: [ "android-ex-camera2", + "flag-junit", "guava", "mockito-target-extended", "androidx.test.rules", @@ -60,6 +62,7 @@ android_test { "androidx.fragment_fragment", "androidx.test.ext.junit", "platform-compat-test-rules", + "telecom_flags_core_java_lib", ], srcs: [ "tests/src/**/*.java", @@ -98,8 +101,8 @@ android_test { platform_apis: true, certificate: "platform", jacoco: { - include_filter: ["com.android.server.telecom.*"], - exclude_filter: ["com.android.server.telecom.tests.*"], + include_filter: ["com.android.server.telecom.**"], + exclude_filter: ["com.android.server.telecom.tests.**"], }, test_suites: ["device-tests"], defaults: ["SettingsLibDefaults"], diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ab067d918..c6f5e9c69 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -28,7 +28,6 @@ <!-- Prevents the activity manager from delaying any activity-start requests by this package, including requests immediately after the user presses "home". --> - <uses-permission android:name="android.permission.BIND_CONNECTION_SERVICE"/> <uses-permission android:name="android.permission.BIND_INCALL_SERVICE"/> <uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> @@ -319,18 +318,6 @@ android:exported="false" android:process=":ui"/> - <service android:name=".components.BluetoothPhoneService" - android:singleUser="true" - android:process="system" - android:exported="true"> - <intent-filter> - <action android:name="android.bluetooth.IBluetoothHeadsetPhone"/> - </intent-filter> - <intent-filter> - <action android:name="android.bluetooth.IBluetoothLeCallControlCallback" /> - </intent-filter> - </service> - <service android:name=".components.TelecomService" android:singleUser="true" android:process="system" @@ -1,7 +1,6 @@ breadley@google.com tgunn@google.com xiaotonj@google.com -chinmayd@google.com tjstuart@google.com rgreenwalt@google.com pmadapurmath@google.com diff --git a/flags/Android.bp b/flags/Android.bp new file mode 100644 index 000000000..6fa147a86 --- /dev/null +++ b/flags/Android.bp @@ -0,0 +1,41 @@ +// +// Copyright (C) 2023 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +aconfig_declarations { + name: "telecom_flags", + package: "com.android.server.telecom.flags", + srcs: [ + "telecom_broadcast_flags.aconfig", + "telecom_ringer_flag_declarations.aconfig", + "telecom_api_flags.aconfig", + "telecom_call_filtering_flags.aconfig", + "telecom_incallservice_flags.aconfig", + "telecom_default_phone_account_flags.aconfig", + "telecom_callaudioroutestatemachine_flags.aconfig", + "telecom_calls_manager_flags.aconfig", + "telecom_anomaly_report_flags.aconfig", + "telecom_callaudiomodestatemachine_flags.aconfig", + "telecom_calllog_flags.aconfig", + "telecom_resolve_hidden_dependencies.aconfig", + "telecom_bluetoothroutemanager_flags.aconfig", + "telecom_work_profile_flags.aconfig" + ], +} + diff --git a/flags/telecom_anomaly_report_flags.aconfig b/flags/telecom_anomaly_report_flags.aconfig new file mode 100644 index 000000000..dbacc08c2 --- /dev/null +++ b/flags/telecom_anomaly_report_flags.aconfig @@ -0,0 +1,8 @@ +package: "com.android.server.telecom.flags" + +flag { + name: "gen_anom_report_on_focus_timeout" + namespace: "telecom" + description: "When getCurrentFocusCall times out, generate an anom. report" + bug: "309541253" +} diff --git a/flags/telecom_api_flags.aconfig b/flags/telecom_api_flags.aconfig new file mode 100644 index 000000000..74cb447a4 --- /dev/null +++ b/flags/telecom_api_flags.aconfig @@ -0,0 +1,29 @@ +package: "com.android.server.telecom.flags" + +flag { + name: "voip_app_actions_support" + namespace: "telecom" + description: "When set, Telecom support for additional VOIP application actions is active." + bug: "296934278" +} + +flag { + name: "call_details_id_changes" + namespace: "telecom" + description: "When set, call details/extras id updates to Telecom APIs for Android V are active." + bug: "301713560" +} + +flag { + name: "unbind_timeout_connections" + namespace: "telecom" + description: "When set, Telecom will auto-unbind if a ConnectionService returns no connections after some time." + bug: "293458004" +} + +flag{ + name: "add_call_uri_for_missed_calls" + namespace: "telecom" + description: "The key is used for dialer apps to mark missed calls as read when it gets the notification on reboot." + bug: "292597423" +} diff --git a/flags/telecom_bluetoothroutemanager_flags.aconfig b/flags/telecom_bluetoothroutemanager_flags.aconfig new file mode 100644 index 000000000..ddd8571c1 --- /dev/null +++ b/flags/telecom_bluetoothroutemanager_flags.aconfig @@ -0,0 +1,9 @@ +package: "com.android.server.telecom.flags" + +flag { + name: "use_actual_address_to_enter_connecting_state" + namespace: "telecom" + description: "Fix bugs that may add bluetooth device with null address." + bug: "306113816" +} + diff --git a/flags/telecom_broadcast_flags.aconfig b/flags/telecom_broadcast_flags.aconfig new file mode 100644 index 000000000..348d57473 --- /dev/null +++ b/flags/telecom_broadcast_flags.aconfig @@ -0,0 +1,8 @@ +package: "com.android.server.telecom.flags" + +flag { + name: "is_new_outgoing_call_broadcast_unblocking" + namespace: "telecom" + description: "When set, the ACTION_NEW_OUTGOING_CALL broadcast is unblocking." + bug: "224550864" +}
\ No newline at end of file diff --git a/flags/telecom_call_filtering_flags.aconfig b/flags/telecom_call_filtering_flags.aconfig new file mode 100644 index 000000000..95e74ce86 --- /dev/null +++ b/flags/telecom_call_filtering_flags.aconfig @@ -0,0 +1,8 @@ +package: "com.android.server.telecom.flags" + +flag { + name: "skip_filter_phone_account_perform_dnd_filter" + namespace: "telecom" + description: "Gates whether to still perform Dnd filter when phone account has skip_filter call extra." + bug: "222333869" +}
\ No newline at end of file diff --git a/flags/telecom_callaudiomodestatemachine_flags.aconfig b/flags/telecom_callaudiomodestatemachine_flags.aconfig new file mode 100644 index 000000000..b26311378 --- /dev/null +++ b/flags/telecom_callaudiomodestatemachine_flags.aconfig @@ -0,0 +1,8 @@ +package: "com.android.server.telecom.flags" + +flag { + name: "set_audio_mode_before_abandon_focus" + namespace: "telecom" + description: "Set audio mode to MODE_NORMAL before abandon the audio focus." + bug: "281841785" +} diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig new file mode 100644 index 000000000..6f2c7fc7d --- /dev/null +++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig @@ -0,0 +1,64 @@ +package: "com.android.server.telecom.flags" + +flag { + name: "available_routes_never_updated_after_set_system_audio_state" + namespace: "telecom" + description: "Fix supported routes wrongly include bluetooth issue." + bug: "292599751" +} + +flag { + name: "use_refactored_audio_route_switching" + namespace: "telecom" + description: "Refactored audio routing" + bug: "306395598" +} + +flag { + name: "ensure_audio_mode_updates_on_foreground_call_change" + namespace: "telecom" + description: "Ensure that the audio mode is updated anytime the foreground call changes." + bug: "289861657" +} + +flag { + name: "ignore_auto_route_to_watch_device" + namespace: "telecom" + description: "Ignore auto routing to wearable devices." + bug: "294378768" +} + +flag { + name: "transit_route_before_audio_disconnect_bt" + namespace: "telecom" + description: "Fix audio route transition issue on call disconnection when bt audio connected." + bug: "306113816" +} + +flag { + name: "call_audio_communication_device_refactor" + namespace: "telecom" + description: "Refactor call audio set/clear communication device and include unsupported routes." + bug: "308968392" +} + +flag { + name: "communication_device_protected_by_lock" + namespace: "telecom" + description: "Protect set/clear communication device operation with lock to avoid race condition." + bug: "303001133" +} + +flag { + name: "reset_mute_when_entering_quiescent_bt_route" + namespace: "telecom" + description: "Reset mute state when entering quiescent bluetooth route." + bug: "311313250" +} + +flag { + name: "update_route_mask_when_bt_connected" + namespace: "telecom" + description: "Update supported route mask when Bluetooth devices audio connected." + bug: "301695370" +} diff --git a/flags/telecom_calllog_flags.aconfig b/flags/telecom_calllog_flags.aconfig new file mode 100644 index 000000000..3ce7b632a --- /dev/null +++ b/flags/telecom_calllog_flags.aconfig @@ -0,0 +1,15 @@ +package: "com.android.server.telecom.flags" + +flag { + name: "telecom_log_external_wearable_calls" + namespace: "telecom" + description: "log external call if current device is a wearable one" + bug: "292600751" +} + +flag { + name: "telecom_skip_log_based_on_extra" + namespace: "telecom" + description: "skipping logging a call based on passed extra" + bug: "295530944" +} diff --git a/flags/telecom_calls_manager_flags.aconfig b/flags/telecom_calls_manager_flags.aconfig new file mode 100644 index 000000000..1a1948039 --- /dev/null +++ b/flags/telecom_calls_manager_flags.aconfig @@ -0,0 +1,15 @@ +package: "com.android.server.telecom.flags" + +flag { + name: "use_improved_listener_order" + namespace: "telecom" + description: "Make InCallController the first listener to trigger" + bug: "24244713" +} + +flag { + name: "fix_audio_flicker_for_outgoing_calls" + namespace: "telecom" + description: "This fix ensures the MO calls won't switch from Active to Quite b/c setDialing was not called" + bug: "309540769" +} diff --git a/flags/telecom_default_phone_account_flags.aconfig b/flags/telecom_default_phone_account_flags.aconfig new file mode 100644 index 000000000..03f324cf3 --- /dev/null +++ b/flags/telecom_default_phone_account_flags.aconfig @@ -0,0 +1,15 @@ +package: "com.android.server.telecom.flags" + +flag { + name: "only_update_telephony_on_valid_sub_ids" + namespace: "telecom" + description: "For testing purposes, only update Telephony when the default calling subId is non-zero" + bug: "234846282" +} + +flag { + name: "telephony_has_default_but_telecom_does_not" + namespace: "telecom" + description: "Telecom is requesting the user to select a sim account to place the outgoing call on but the user has a default account in the settings" + bug: "302397094" +}
\ No newline at end of file diff --git a/flags/telecom_incallservice_flags.aconfig b/flags/telecom_incallservice_flags.aconfig new file mode 100644 index 000000000..e1a652ba8 --- /dev/null +++ b/flags/telecom_incallservice_flags.aconfig @@ -0,0 +1,15 @@ +package: "com.android.server.telecom.flags" + +flag { + name: "early_binding_to_incall_service" + namespace: "telecom" + description: "Binds to InCallServices when call requires no call filtering on watch" + bug: "282113261" +} + +flag { + name: "ecc_keyguard" + namespace: "telecom" + description: "Ensure that users are able to return to call from keyguard UI for ECC" + bug: "306582821" +}
\ No newline at end of file diff --git a/flags/telecom_resolve_hidden_dependencies.aconfig b/flags/telecom_resolve_hidden_dependencies.aconfig new file mode 100644 index 000000000..ecc01238a --- /dev/null +++ b/flags/telecom_resolve_hidden_dependencies.aconfig @@ -0,0 +1,8 @@ +package: "com.android.server.telecom.flags" + +flag { + name: "telecom_resolve_hidden_dependencies" + namespace: "android_platform_telecom" + description: "Mainland cleanup for hidden dependencies" + bug: "b/303440370" +} diff --git a/flags/telecom_ringer_flag_declarations.aconfig b/flags/telecom_ringer_flag_declarations.aconfig new file mode 100644 index 000000000..54748d0b0 --- /dev/null +++ b/flags/telecom_ringer_flag_declarations.aconfig @@ -0,0 +1,8 @@ +package: "com.android.server.telecom.flags" + +flag { + name: "use_device_provided_serialized_ringer_vibration" + namespace: "telecom" + description: "Gates whether to use a serialized, device-specific ring vibration." + bug: "282113261" +}
\ No newline at end of file diff --git a/flags/telecom_work_profile_flags.aconfig b/flags/telecom_work_profile_flags.aconfig new file mode 100644 index 000000000..cc78b3083 --- /dev/null +++ b/flags/telecom_work_profile_flags.aconfig @@ -0,0 +1,8 @@ +package: "com.android.server.telecom.flags" + +flag { + name: "work_profile_associated_user" + namespace: "telecom" + description: "Redefines the associated user for calls in the context of work profile support (U+)" + bug: "294699269" +}
\ No newline at end of file diff --git a/proguard.flags b/proguard.flags index 635eba685..7c71a157b 100644 --- a/proguard.flags +++ b/proguard.flags @@ -9,17 +9,3 @@ -keep class android.telecom.Log { *; } - -# Keep classes, annotations and members used by Lifecycle. Remove this once aapt2 is enabled --keepattributes *Annotation* - --keep class * implements android.arch.lifecycle.LifecycleObserver { -} - --keep class * implements android.arch.lifecycle.GeneratedAdapter { - <init>(...); -} - --keepclassmembers class ** { - @android.arch.lifecycle.OnLifecycleEvent *; -} diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml index 21c8d7cc3..f0923d5ca 100644 --- a/res/values-am/strings.xml +++ b/res/values-am/strings.xml @@ -128,7 +128,7 @@ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ማዳመጫ"</string> <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"ብሉቱዝ"</string> <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"ባለገመድ ማዳመጫ"</string> - <string name="callendpoint_name_speaker" msgid="1971760468695323189">"ድምጽ ማውጫ"</string> + <string name="callendpoint_name_speaker" msgid="1971760468695323189">"ድምፅ ማውጫ"</string> <string name="callendpoint_name_streaming" msgid="2337595450408275576">"ውጫዊ"</string> <string name="callendpoint_name_unknown" msgid="2199074708477193852">"ያልታወቀ"</string> <string name="call_streaming_notification_body" msgid="502216105683378263">"ኦዲዮን ወደ ሌላ መሣሪያ በመልቀቅ ላይ"</string> diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index 2cf961d4d..6b58863e8 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -101,7 +101,7 @@ <string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Αποσυνδεδεμένες κλήσεις"</string> <string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Εφαρμογές τηλεφώνου που αντιμετώπισαν σφάλμα λειτουργίας"</string> <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Ροή κλήσης"</string> - <string name="alert_outgoing_call" msgid="5319895109298927431">"Εάν πραγματοποιήσετε αυτήν την κλήση, η κλήση σας μέσω <xliff:g id="OTHER_APP">%1$s</xliff:g> θα τερματιστεί."</string> + <string name="alert_outgoing_call" msgid="5319895109298927431">"Εάν πραγματοποιήσετε αυτή την κλήση, η κλήση σας μέσω <xliff:g id="OTHER_APP">%1$s</xliff:g> θα τερματιστεί."</string> <string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Επιλέξτε πώς θα πραγματοποιήσετε την κλήση"</string> <string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Ανακατεύθυνση της κλήσης μέσω <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string> <string name="alert_place_unredirect_outgoing_call" msgid="2467608535225764006">"Κλήση μέσω του αριθμού τηλεφώνου μου"</string> diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml index 628b44025..399da2030 100644 --- a/res/values-kk/strings.xml +++ b/res/values-kk/strings.xml @@ -69,7 +69,7 @@ <string name="unblock_button" msgid="8732021675729981781">"Бөгеуден шығару"</string> <string name="add_blocked_dialog_body" msgid="8599974422407139255">"Қоңыраулары мен мәтіндік хабарлары бөгелетін нөмір"</string> <string name="add_blocked_number_hint" msgid="8769422085658041097">"Телефон нөмірі"</string> - <string name="block_button" msgid="485080149164258770">"Бөгеу"</string> + <string name="block_button" msgid="485080149164258770">"Блоктау"</string> <string name="non_primary_user" msgid="315564589279622098">"Бөгелген нөмірлерді тек құрылғы иесі көре және басқара алады."</string> <string name="delete_icon_description" msgid="5335959254954774373">"Бөгеуді алу"</string> <string name="blocked_numbers_butter_bar_title" msgid="582982373755950791">"Тыйым уақытша алынды"</string> diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml index e68f3c8e4..5fbe1d3aa 100644 --- a/res/values-pt-rPT/strings.xml +++ b/res/values-pt-rPT/strings.xml @@ -65,7 +65,7 @@ <string name="blocked_numbers" msgid="8322134197039865180">"Números bloqueados"</string> <string name="blocked_numbers_msg" msgid="2797422132329662697">"Não irá receber chamadas ou mensagens de texto de números bloqueados."</string> <string name="block_number" msgid="3784343046852802722">"Adicionar um número"</string> - <string name="unblock_dialog_body" msgid="2723393535797217261">"Pretende desbloquear <xliff:g id="NUMBER_TO_BLOCK">%1$s</xliff:g>?"</string> + <string name="unblock_dialog_body" msgid="2723393535797217261">"Quer desbloquear <xliff:g id="NUMBER_TO_BLOCK">%1$s</xliff:g>?"</string> <string name="unblock_button" msgid="8732021675729981781">"Desbloquear"</string> <string name="add_blocked_dialog_body" msgid="8599974422407139255">"Bloquear chamadas e mensagens de texto de"</string> <string name="add_blocked_number_hint" msgid="8769422085658041097">"Número de telefone"</string> @@ -102,7 +102,7 @@ <string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Apps Telefone com falhas"</string> <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Streaming de chamadas"</string> <string name="alert_outgoing_call" msgid="5319895109298927431">"Ao efetuar esta chamada, irá terminar a chamada na app <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string> - <string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Escolha como pretende efetuar esta chamada"</string> + <string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Escolha como quer efetuar esta chamada"</string> <string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Redirecionar chamada através de <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string> <string name="alert_place_unredirect_outgoing_call" msgid="2467608535225764006">"Ligar com o meu número de telefone"</string> <string name="alert_redirect_outgoing_call_timeout" msgid="5568101425637373060">"Não é possível efetuar uma chamada através da app <xliff:g id="OTHER_APP">%1$s</xliff:g>. Experimente utilizar uma app de redirecionamento de chamadas diferente ou contactar o programador para obter ajuda."</string> diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml index afb8eca46..a7fc3c741 100644 --- a/res/values-pt/strings.xml +++ b/res/values-pt/strings.xml @@ -57,7 +57,7 @@ <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"Definir padrão"</string> <string name="change_default_dialer_dialog_negative" msgid="8648669840052697821">"Cancelar"</string> <string name="change_default_dialer_warning_message" msgid="8461963987376916114">"O <xliff:g id="NEW_APP">%s</xliff:g> poderá ligar e controlar todos os aspectos das chamadas. Defina como aplicativo Telefone padrão somente aqueles em que você confia."</string> - <string name="change_default_call_screening_dialog_title" msgid="5365787219927262408">"Usar o <xliff:g id="NEW_APP">%s</xliff:g> como seu app de seleção de chamadas padrão?"</string> + <string name="change_default_call_screening_dialog_title" msgid="5365787219927262408">"Usar o <xliff:g id="NEW_APP">%s</xliff:g> como seu app de filtro de ligações padrão?"</string> <string name="change_default_call_screening_warning_message_for_disable_old_app" msgid="2039830033533243164">"O <xliff:g id="OLD_APP">%s</xliff:g> não selecionará mais as chamadas."</string> <string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"O <xliff:g id="NEW_APP">%s</xliff:g> poderá ver as informações sobre os autores das chamadas que não estão entre seus contatos e bloqueá-los. Defina como app de seleção de chamadas padrão somente aqueles em que você confia."</string> <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Definir padrão"</string> diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index 071289e41..1ef0a552f 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -45,7 +45,7 @@ <string name="respond_via_sms_edittext_dialog_title" msgid="6579353156073272157">"快速回复"</string> <string name="respond_via_sms_confirmation_format" msgid="2932395476561267842">"讯息已发送至 <xliff:g id="PHONE_NUMBER">%s</xliff:g>。"</string> <string name="respond_via_sms_failure_format" msgid="5198680980054596391">"未能将信息发送到 <xliff:g id="PHONE_NUMBER">%s</xliff:g>。"</string> - <string name="enable_account_preference_title" msgid="6949224486748457976">"通话帐号"</string> + <string name="enable_account_preference_title" msgid="6949224486748457976">"通话账号"</string> <string name="outgoing_call_not_allowed_user_restriction" msgid="3424338207838851646">"只能拨打紧急呼救电话。"</string> <string name="outgoing_call_not_allowed_no_permission" msgid="8590468836581488679">"此应用没有电话权限,无法拨出电话。"</string> <string name="outgoing_call_error_no_phone_number_supplied" msgid="7665135102566099778">"要拨打电话,请输入有效的电话号码。"</string> @@ -90,7 +90,7 @@ <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"如果接听此来电,您当前的视频通话会中断。"</string> <string name="answer_incoming_call" msgid="2045888814782215326">"接听"</string> <string name="decline_incoming_call" msgid="922147089348451310">"拒接"</string> - <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"无法拨出电话,因为没有通话帐号支持拨打这类电话。"</string> + <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"无法拨出电话,因为没有通话账号支持拨打这类电话。"</string> <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"由于当前正在进行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通话,因此无法拨打电话。"</string> <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"由于当前正在进行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通话,因此无法拨打电话。"</string> <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"由于当前正在通过其他应用通话,因此无法拨打电话。"</string> diff --git a/res/values/config.xml b/res/values/config.xml index 15f765bae..c38a6ec4e 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -49,8 +49,10 @@ <bool name="grant_location_permission_enabled">false</bool> <!-- When true, a simple full intensity on/off vibration pattern will be used when calls ring. - When false, a fancy vibration pattern which ramps up and down will be used. - Devices should overlay this value based on the type of vibration hardware they employ. --> + + When false, the vibration effect serialized in the raw `default_ringtone_vibration_effect` + resource (under `frameworks/base/core/res/res/raw/`) is used. Devices should overlay this + value based on the type of vibration hardware they employ. --> <bool name="use_simple_vibration_pattern">false</bool> <!-- Threshold for the X+Y component of gravity needed for the device orientation to be diff --git a/res/xml/activity_blocked_numbers.xml b/res/xml/activity_blocked_numbers.xml index e77184dea..b6298e96d 100644 --- a/res/xml/activity_blocked_numbers.xml +++ b/res/xml/activity_blocked_numbers.xml @@ -41,8 +41,8 @@ android:layout_height="wrap_content" android:text="@string/non_primary_user" android:paddingTop="@dimen/blocked_numbers_large_padding" - android:paddingLeft="@dimen/blocked_numbers_large_padding" - android:paddingRight="@dimen/blocked_numbers_large_padding" + android:paddingStart="@dimen/blocked_numbers_large_padding" + android:paddingEnd="@dimen/blocked_numbers_large_padding" style="@style/BlockedNumbersTextPrimary2" android:visibility="gone" /> @@ -62,8 +62,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="@dimen/blocked_numbers_large_padding" - android:paddingLeft="@dimen/blocked_numbers_large_padding" - android:paddingRight="@dimen/blocked_numbers_large_padding"> + android:paddingStart="@dimen/blocked_numbers_large_padding" + android:paddingEnd="@dimen/blocked_numbers_large_padding"> <TextView android:layout_width="wrap_content" diff --git a/res/xml/blocking_suppressed_butterbar.xml b/res/xml/blocking_suppressed_butterbar.xml index 8b941b99e..29473406f 100644 --- a/res/xml/blocking_suppressed_butterbar.xml +++ b/res/xml/blocking_suppressed_butterbar.xml @@ -25,19 +25,19 @@ android:id="@+id/icon" android:layout_height="wrap_content" android:layout_width="wrap_content" - android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" android:paddingTop="@dimen/blocked_numbers_large_padding" - android:paddingRight="@dimen/blocked_numbers_large_padding" - android:paddingLeft="@dimen/blocked_numbers_large_padding" + android:paddingEnd="@dimen/blocked_numbers_large_padding" + android:paddingStart="@dimen/blocked_numbers_large_padding" android:src="@drawable/ic_status_blocked_orange_40dp"/> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_toRightOf="@id/icon" + android:layout_toEndOf="@id/icon" android:paddingTop="@dimen/blocked_numbers_large_padding" - android:paddingRight="@dimen/blocked_numbers_large_padding" + android:paddingEnd="@dimen/blocked_numbers_large_padding" android:text="@string/blocked_numbers_butter_bar_title" style="@style/BlockedNumbersTextPrimary2" /> @@ -45,11 +45,11 @@ android:id="@+id/description" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_toRightOf="@id/icon" + android:layout_toEndOf="@id/icon" android:layout_below="@id/title" android:paddingTop="@dimen/blocked_numbers_large_padding" android:paddingBottom="@dimen/blocked_numbers_large_padding" - android:paddingRight="@dimen/blocked_numbers_large_padding" + android:paddingEnd="@dimen/blocked_numbers_large_padding" android:text="@string/blocked_numbers_butter_bar_body" style="@style/BlockedNumbersTextSecondary" /> @@ -57,9 +57,9 @@ android:id="@+id/reenable_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_toRightOf="@id/icon" + android:layout_toEndOf="@id/icon" android:layout_below="@id/description" - android:paddingRight="@dimen/blocked_numbers_large_padding" + android:paddingEnd="@dimen/blocked_numbers_large_padding" android:text="@string/blocked_numbers_butter_bar_button" style="@style/BlockedNumbersButton" android:background="?android:attr/selectableItemBackgroundBorderless" /> diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java index c32d2bf11..7ef2a12a4 100644 --- a/src/com/android/server/telecom/Call.java +++ b/src/com/android/server/telecom/Call.java @@ -17,12 +17,13 @@ package com.android.server.telecom; import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED; -import static android.telecom.Call.EVENT_DISPLAY_SOS_MESSAGE; +import static android.telephony.TelephonyManager.EVENT_DISPLAY_EMERGENCY_MESSAGE; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -30,6 +31,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.os.OutcomeReceiver; import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.RemoteException; @@ -43,6 +45,7 @@ import android.telecom.CallAttributes; import android.telecom.CallAudioState; import android.telecom.CallDiagnosticService; import android.telecom.CallDiagnostics; +import android.telecom.CallException; import android.telecom.CallerInfo; import android.telecom.Conference; import android.telecom.Connection; @@ -70,9 +73,13 @@ import android.widget.Toast; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telecom.IVideoProvider; import com.android.internal.util.Preconditions; +import com.android.server.telecom.flags.FeatureFlags; import com.android.server.telecom.stats.CallFailureCause; import com.android.server.telecom.stats.CallStateChangedAtomWriter; import com.android.server.telecom.ui.ToastFactory; +import com.android.server.telecom.voip.TransactionManager; +import com.android.server.telecom.voip.VerifyCallStateChangeTransaction; +import com.android.server.telecom.voip.VoipCallTransactionResult; import java.io.IOException; import java.text.SimpleDateFormat; @@ -118,6 +125,24 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, private static final char NO_DTMF_TONE = '\0'; + + /** + * Listener for CallState changes which can be leveraged by a Transaction. + */ + public interface CallStateListener { + void onCallStateChanged(int newCallState); + } + + public List<CallStateListener> mCallStateListeners = new ArrayList<>(); + + public void addCallStateListener(CallStateListener newListener) { + mCallStateListeners.add(newListener); + } + + public boolean removeCallStateListener(CallStateListener newListener) { + return mCallStateListeners.remove(newListener); + } + /** * Listener for events on the call. */ @@ -283,18 +308,25 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, @Override public void onCallerInfoQueryComplete(Uri handle, CallerInfo callerInfo) { synchronized (mLock) { - Call.this.setCallerInfo(handle, callerInfo); + Call call = Call.this; + if (call != null) { + call.setCallerInfo(handle, callerInfo); + } } } @Override public void onContactPhotoQueryComplete(Uri handle, CallerInfo callerInfo) { synchronized (mLock) { - Call.this.setCallerInfo(handle, callerInfo); + Call call = Call.this; + if (call != null) { + call.setCallerInfo(handle, callerInfo); + } } } }; + private final boolean mIsModifyStatePermissionGranted; /** * One of CALL_DIRECTION_INCOMING, CALL_DIRECTION_OUTGOING, or CALL_DIRECTION_UNKNOWN */ @@ -757,6 +789,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, */ private CompletableFuture<Boolean> mDisconnectFuture; + private FeatureFlags mFlags; + /** * Persists the specified parameters and initializes the new instance. * @param context The context. @@ -788,11 +822,12 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, boolean shouldAttachToExistingConnection, boolean isConference, ClockProxy clockProxy, - ToastFactory toastFactory) { + ToastFactory toastFactory, + FeatureFlags featureFlags) { this(callId, context, callsManager, lock, repository, phoneNumberUtilsAdapter, handle, null, gatewayInfo, connectionManagerPhoneAccountHandle, targetPhoneAccountHandle, callDirection, shouldAttachToExistingConnection, - isConference, clockProxy, toastFactory); + isConference, clockProxy, toastFactory, featureFlags); } @@ -812,8 +847,10 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, boolean shouldAttachToExistingConnection, boolean isConference, ClockProxy clockProxy, - ToastFactory toastFactory) { + ToastFactory toastFactory, + FeatureFlags featureFlags) { + mFlags = featureFlags; mId = callId; mConnectionId = callId; mState = (isConference && callDirection != CALL_DIRECTION_INCOMING && @@ -844,6 +881,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, mStartRingTime = 0; mCallStateChangedAtomWriter.setExistingCallCount(callsManager.getCalls().size()); + mIsModifyStatePermissionGranted = + isModifyPhoneStatePermissionGranted(getDelegatePhoneAccountHandle()); } /** @@ -863,6 +902,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * connection, regardless of whether it's incoming or outgoing. * @param connectTimeMillis The connection time of the call. * @param clockProxy + * @param featureFlags The telecom feature flags. */ Call( String callId, @@ -881,11 +921,13 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, long connectTimeMillis, long connectElapsedTimeMillis, ClockProxy clockProxy, - ToastFactory toastFactory) { + ToastFactory toastFactory, + FeatureFlags featureFlags) { this(callId, context, callsManager, lock, repository, phoneNumberUtilsAdapter, handle, gatewayInfo, connectionManagerPhoneAccountHandle, targetPhoneAccountHandle, callDirection, - shouldAttachToExistingConnection, isConference, clockProxy, toastFactory); + shouldAttachToExistingConnection, isConference, clockProxy, toastFactory, + featureFlags); mConnectTimeMillis = connectTimeMillis; mConnectElapsedTimeMillis = connectElapsedTimeMillis; @@ -1328,6 +1370,10 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, Log.addEvent(this, event, stringData); } + for (CallStateListener listener : mCallStateListeners) { + listener.onCallStateChanged(newState); + } + mCallStateChangedAtomWriter .setDisconnectCause(getDisconnectCause()) .setSelfManaged(isSelfManaged()) @@ -1733,8 +1779,12 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, accountHandle.getComponentName().getPackageName(), mContext.getPackageManager()); // Set the associated user for the call for MT calls based on the target phone account. - if (isIncoming() && !accountHandle.getUserHandle().equals(mAssociatedUser)) { - setAssociatedUser(accountHandle.getUserHandle()); + UserHandle associatedUser = UserUtil.getAssociatedUserForCall( + mFlags.workProfileAssociatedUser(), + mCallsManager.getPhoneAccountRegistrar(), mCallsManager.getCurrentUserHandle(), + accountHandle); + if (isIncoming() && !associatedUser.equals(mAssociatedUser)) { + setAssociatedUser(associatedUser); } } } @@ -2898,11 +2948,16 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, hold(null /* reason */); } + /** + * This method requests the ConnectionService or TransactionalService hosting the call to put + * the call on hold + */ public void hold(String reason) { if (mState == CallState.ACTIVE) { if (mTransactionalService != null) { mTransactionalService.onSetInactive(this); } else if (mConnectionService != null) { + awaitCallStateChangeAndMaybeDisconnectCall(CallState.ON_HOLD, isSelfManaged(), "hold"); mConnectionService.hold(this); } else { Log.e(this, new NullPointerException(), @@ -2913,6 +2968,27 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, } /** + * helper that can be used for any callback that requests a call state change and wants to + * verify the change + */ + public void awaitCallStateChangeAndMaybeDisconnectCall(int targetCallState, + boolean shouldDisconnectUponTimeout, String callingMethod) { + TransactionManager tm = TransactionManager.getInstance(); + tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager, + this, targetCallState, shouldDisconnectUponTimeout), new OutcomeReceiver<>() { + @Override + public void onResult(VoipCallTransactionResult result) { + } + + @Override + public void onError(CallException e) { + Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onError" + + " due to CallException=[%s]", callingMethod, e); + } + }); + } + + /** * Releases the call from hold if it is currently active. */ @VisibleForTesting @@ -3039,6 +3115,12 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, Connection.EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE)); } + if (mExtras.containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL)) { + if (source != SOURCE_CONNECTION_SERVICE || !mIsModifyStatePermissionGranted) { + mExtras.remove(TelecomManager.EXTRA_DO_NOT_LOG_CALL); + } + } + // If the change originated from an InCallService, notify the connection service. if (source == SOURCE_INCALL_SERVICE) { Log.addEvent(this, LogUtils.Events.ICS_EXTRAS_CHANGED); @@ -3053,6 +3135,15 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, } } + private boolean isModifyPhoneStatePermissionGranted(PhoneAccountHandle phoneAccountHandle) { + if (phoneAccountHandle == null) { + return false; + } + String packageName = phoneAccountHandle.getComponentName().getPackageName(); + return PackageManager.PERMISSION_GRANTED == mContext.getPackageManager().checkPermission( + android.Manifest.permission.MODIFY_PHONE_STATE, packageName); + } + /** * Removes extras from the extras bundle associated with this {@link Call}. * @@ -3663,7 +3754,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, } String newName = callerInfo.getName(); - boolean contactNameChanged = mCallerInfo == null || !mCallerInfo.getName().equals(newName); + boolean contactNameChanged = mCallerInfo == null || + !Objects.equals(mCallerInfo.getName(), newName); mCallerInfo = callerInfo; Log.i(this, "CallerInfo received for %s: %s", Log.piiHandle(mHandle), callerInfo); @@ -4032,7 +4124,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, * @param associatedUser */ public void setAssociatedUser(UserHandle associatedUser) { - Log.i(this, "Setting associated user for call"); + Log.i(this, "Setting associated user for call: %s", associatedUser); Preconditions.checkNotNull(associatedUser); mAssociatedUser = associatedUser; } @@ -4182,8 +4274,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable, l.onReceivedCallQualityReport(this, callQuality); } } else { - if (event.equals(EVENT_DISPLAY_SOS_MESSAGE) && !isEmergencyCall()) { - Log.w(this, "onConnectionEvent: EVENT_DISPLAY_SOS_MESSAGE is sent " + if (event.equals(EVENT_DISPLAY_EMERGENCY_MESSAGE) && !isEmergencyCall()) { + Log.w(this, "onConnectionEvent: EVENT_DISPLAY_EMERGENCY_MESSAGE is sent " + "without an emergency call"); return; } diff --git a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java index 43624a3c3..5fc241446 100644 --- a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java +++ b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java @@ -24,10 +24,12 @@ import android.telecom.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.server.telecom.bluetooth.BluetoothRouteManager; +import com.android.server.telecom.flags.Flags; import java.util.Arrays; import java.util.List; import java.util.Set; +import java.util.concurrent.Semaphore; /** * Helper class used to keep track of the requested communication device within Telecom for audio @@ -50,6 +52,7 @@ public class CallAudioCommunicationDeviceTracker { private int mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID; // Keep track of the locally requested BT audio device if set private String mBtAudioDevice = null; + private final Semaphore mLock = new Semaphore(1); public CallAudioCommunicationDeviceTracker(Context context) { mAudioManager = context.getSystemService(AudioManager.class); @@ -63,6 +66,10 @@ public class CallAudioCommunicationDeviceTracker { return mAudioDeviceType == audioDeviceType; } + public int getCurrentLocallyRequestedCommunicationDevice() { + return mAudioDeviceType; + } + @VisibleForTesting public void setTestCommunicationDevice(int audioDeviceType) { mAudioDeviceType = audioDeviceType; @@ -90,6 +97,9 @@ public class CallAudioCommunicationDeviceTracker { */ public boolean setCommunicationDevice(int audioDeviceType, BluetoothDevice btDevice) { + if (Flags.communicationDeviceProtectedByLock()) { + mLock.tryAcquire(); + } // There is only one audio device type associated with each type of BT device. boolean isBtDevice = sBT_AUDIO_DEVICE_TYPES.contains(audioDeviceType); Log.i(this, "setCommunicationDevice: type = %s, isBtDevice = %s, btDevice = %s", @@ -149,8 +159,16 @@ public class CallAudioCommunicationDeviceTracker { if (audioDeviceType == AudioDeviceInfo.TYPE_BLE_HEADSET) { mBluetoothRouteManager.onAudioOn(mBtAudioDevice); } + } else if (Flags.communicationDeviceProtectedByLock()) { + // Clear BT device if it's still stored. Handles race condition for when a non-BT + // device is set for communication shortly after a BT (LE) device is set for + // communication but the selection hasn't been cleared yet. + mBtAudioDevice = null; } } + if (Flags.communicationDeviceProtectedByLock()) { + mLock.release(); + } return result; } @@ -160,6 +178,9 @@ public class CallAudioCommunicationDeviceTracker { * @param audioDeviceTypes The supported audio device types for the device. */ public void clearCommunicationDevice(int audioDeviceType) { + if (Flags.communicationDeviceProtectedByLock()) { + mLock.tryAcquire(); + } // There is only one audio device type associated with each type of BT device. boolean isBtDevice = sBT_AUDIO_DEVICE_TYPES.contains(audioDeviceType); Log.i(this, "clearCommunicationDevice: type = %s, isBtDevice = %s", @@ -177,12 +198,6 @@ public class CallAudioCommunicationDeviceTracker { return; } - if (isBtDevice && mBtAudioDevice != null) { - // Signal that BT audio was lost for device. - mBluetoothRouteManager.onAudioLost(mBtAudioDevice); - mBtAudioDevice = null; - } - if (mAudioManager == null) { Log.i(this, "clearCommunicationDevice: mAudioManager is null"); return; @@ -191,6 +206,15 @@ public class CallAudioCommunicationDeviceTracker { // Clear device and reset locally saved device type. mAudioManager.clearCommunicationDevice(); mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID; + + if (isBtDevice && mBtAudioDevice != null) { + // Signal that BT audio was lost for device. + mBluetoothRouteManager.onAudioLost(mBtAudioDevice); + mBtAudioDevice = null; + } + if (Flags.communicationDeviceProtectedByLock()) { + mLock.release(); + } } private boolean isUsbHeadsetType(int audioDeviceType, int sourceType) { diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java index 6557dc638..2201c2fa6 100644 --- a/src/com/android/server/telecom/CallAudioManager.java +++ b/src/com/android/server/telecom/CallAudioManager.java @@ -23,7 +23,6 @@ import android.media.ToneGenerator; import android.os.UserHandle; import android.telecom.CallAudioState; import android.telecom.Log; -import android.telecom.PhoneAccount; import android.telecom.VideoProfile; import android.util.SparseArray; @@ -31,6 +30,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs.Builder; import com.android.server.telecom.bluetooth.BluetoothStateReceiver; +import com.android.server.telecom.flags.FeatureFlags; import java.util.Collection; import java.util.HashSet; @@ -54,7 +54,7 @@ public class CallAudioManager extends CallsManagerListenerBase { private final Set<Call> mCalls; private final SparseArray<LinkedHashSet<Call>> mCallStateToCalls; - private final CallAudioRouteStateMachine mCallAudioRouteStateMachine; + private final CallAudioRouteAdapter mCallAudioRouteAdapter; private final CallAudioModeStateMachine mCallAudioModeStateMachine; private final BluetoothStateReceiver mBluetoothStateReceiver; private final CallsManager mCallsManager; @@ -62,6 +62,7 @@ public class CallAudioManager extends CallsManagerListenerBase { private final Ringer mRinger; private final RingbackPlayer mRingbackPlayer; private final DtmfLocalTonePlayer mDtmfLocalTonePlayer; + private final FeatureFlags mFeatureFlags; private Call mStreamingCall; private Call mForegroundCall; @@ -69,14 +70,15 @@ public class CallAudioManager extends CallsManagerListenerBase { private boolean mIsDisconnectedTonePlaying = false; private InCallTonePlayer mHoldTonePlayer; - public CallAudioManager(CallAudioRouteStateMachine callAudioRouteStateMachine, + public CallAudioManager(CallAudioRouteAdapter callAudioRouteAdapter, CallsManager callsManager, CallAudioModeStateMachine callAudioModeStateMachine, InCallTonePlayer.Factory playerFactory, Ringer ringer, RingbackPlayer ringbackPlayer, BluetoothStateReceiver bluetoothStateReceiver, - DtmfLocalTonePlayer dtmfLocalTonePlayer) { + DtmfLocalTonePlayer dtmfLocalTonePlayer, + FeatureFlags featureFlags) { mActiveDialingOrConnectingCalls = new LinkedHashSet<>(1); mRingingCalls = new LinkedHashSet<>(1); mHoldingCalls = new LinkedHashSet<>(1); @@ -94,7 +96,7 @@ public class CallAudioManager extends CallsManagerListenerBase { put(CallState.AUDIO_PROCESSING, mAudioProcessingCalls); }}; - mCallAudioRouteStateMachine = callAudioRouteStateMachine; + mCallAudioRouteAdapter = callAudioRouteAdapter; mCallAudioModeStateMachine = callAudioModeStateMachine; mCallsManager = callsManager; mPlayerFactory = playerFactory; @@ -102,10 +104,11 @@ public class CallAudioManager extends CallsManagerListenerBase { mRingbackPlayer = ringbackPlayer; mBluetoothStateReceiver = bluetoothStateReceiver; mDtmfLocalTonePlayer = dtmfLocalTonePlayer; + mFeatureFlags = featureFlags; mPlayerFactory.setCallAudioManager(this); mCallAudioModeStateMachine.setCallAudioManager(this); - mCallAudioRouteStateMachine.setCallAudioManager(this); + mCallAudioRouteAdapter.setCallAudioManager(this); } @Override @@ -222,7 +225,7 @@ public class CallAudioManager extends CallsManagerListenerBase { // When pulling a video call, automatically enable the speakerphone. Log.d(LOG_TAG, "Switching to speaker because external video call %s was pulled." + call.getId()); - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.SWITCH_SPEAKER); } } @@ -374,7 +377,7 @@ public class CallAudioManager extends CallsManagerListenerBase { @Override public void onConnectionServiceChanged(Call call, ConnectionServiceWrapper oldCs, ConnectionServiceWrapper newCs) { - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE); } @@ -392,13 +395,13 @@ public class CallAudioManager extends CallsManagerListenerBase { Log.d(LOG_TAG, "Switching to speaker because call %s transitioned video state from %s" + " to %s", call.getId(), VideoProfile.videoStateToString(previousVideoState), VideoProfile.videoStateToString(newVideoState)); - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.SWITCH_SPEAKER); } } public CallAudioState getCallAudioState() { - return mCallAudioRouteStateMachine.getCurrentCallAudioState(); + return mCallAudioRouteAdapter.getCurrentCallAudioState(); } public Call getPossiblyHeldForegroundCall() { @@ -419,7 +422,7 @@ public class CallAudioManager extends CallsManagerListenerBase { Log.v(this, "ignoring toggleMute for emergency call"); return; } - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.TOGGLE_MUTE); } @@ -439,7 +442,7 @@ public class CallAudioManager extends CallsManagerListenerBase { Log.v(this, "ignoring mute for emergency call"); } - mCallAudioRouteStateMachine.sendMessageWithSessionInfo(shouldMute + mCallAudioRouteAdapter.sendMessageWithSessionInfo(shouldMute ? CallAudioRouteStateMachine.MUTE_ON : CallAudioRouteStateMachine.MUTE_OFF); } @@ -455,23 +458,23 @@ public class CallAudioManager extends CallsManagerListenerBase { Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route)); switch (route) { case CallAudioState.ROUTE_BLUETOOTH: - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.USER_SWITCH_BLUETOOTH, 0, bluetoothAddress); return; case CallAudioState.ROUTE_SPEAKER: - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.USER_SWITCH_SPEAKER); return; case CallAudioState.ROUTE_WIRED_HEADSET: - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.USER_SWITCH_HEADSET); return; case CallAudioState.ROUTE_EARPIECE: - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.USER_SWITCH_EARPIECE); return; case CallAudioState.ROUTE_WIRED_OR_EARPIECE: - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.USER_SWITCH_BASELINE_ROUTE, CallAudioRouteStateMachine.NO_INCLUDE_BLUETOOTH_IN_BASELINE); return; @@ -486,7 +489,7 @@ public class CallAudioManager extends CallsManagerListenerBase { */ void switchBaseline() { Log.i(this, "switchBaseline"); - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.USER_SWITCH_BASELINE_ROUTE, CallAudioRouteStateMachine.INCLUDE_BLUETOOTH_IN_BASELINE); } @@ -530,7 +533,7 @@ public class CallAudioManager extends CallsManagerListenerBase { synchronized (mCallsManager.getLock()) { Call localForegroundCall = mForegroundCall; boolean result = mRinger.startRinging(localForegroundCall, - mCallAudioRouteStateMachine.isHfpDeviceAvailable()); + mCallAudioRouteAdapter.isHfpDeviceAvailable()); if (result) { localForegroundCall.setStartRingTime(); } @@ -563,7 +566,7 @@ public class CallAudioManager extends CallsManagerListenerBase { @VisibleForTesting public void setCallAudioRouteFocusState(int focusState) { - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.SWITCH_FOCUS, focusState); } @@ -573,8 +576,8 @@ public class CallAudioManager extends CallsManagerListenerBase { } @VisibleForTesting - public CallAudioRouteStateMachine getCallAudioRouteStateMachine() { - return mCallAudioRouteStateMachine; + public CallAudioRouteAdapter getCallAudioRouteAdapter() { + return mCallAudioRouteAdapter; } @VisibleForTesting @@ -611,9 +614,9 @@ public class CallAudioManager extends CallsManagerListenerBase { mCallAudioModeStateMachine.dump(pw); pw.decreaseIndent(); - pw.println("CallAudioRouteStateMachine:"); + pw.println("mCallAudioRouteAdapter:"); pw.increaseIndent(); - mCallAudioRouteStateMachine.dump(pw); + mCallAudioRouteAdapter.dump(pw); pw.decreaseIndent(); pw.println("BluetoothDeviceManager:"); @@ -772,20 +775,26 @@ public class CallAudioManager extends CallsManagerListenerBase { possibleConnectingCall = call; } } - // Prefer a connecting call - if (possibleConnectingCall != null) { - mForegroundCall = possibleConnectingCall; + if (mFeatureFlags.ensureAudioModeUpdatesOnForegroundCallChange()) { + // Prefer a connecting call + if (possibleConnectingCall != null) { + mForegroundCall = possibleConnectingCall; + } else { + // Next, prefer an active or dialing call which is not in the process of being + // disconnected. + mForegroundCall = mActiveDialingOrConnectingCalls + .stream() + .filter(c -> (c.getState() == CallState.ACTIVE + || c.getState() == CallState.DIALING) + && !c.isLocallyDisconnecting()) + .findFirst() + // If we can't find one, then just fall back to the first one. + .orElse(mActiveDialingOrConnectingCalls.iterator().next()); + } } else { - // Next, prefer an active or dialing call which is not in the process of being - // disconnected. - mForegroundCall = mActiveDialingOrConnectingCalls - .stream() - .filter(c -> (c.getState() == CallState.ACTIVE - || c.getState() == CallState.DIALING) - && !c.isLocallyDisconnecting()) - .findFirst() - // If we can't find one, then just fall back to the first one. - .orElse(mActiveDialingOrConnectingCalls.iterator().next()); + // Legacy (buggy) behavior. + mForegroundCall = possibleConnectingCall == null ? + mActiveDialingOrConnectingCalls.iterator().next() : possibleConnectingCall; } } else if (mRingingCalls.size() > 0) { mForegroundCall = mRingingCalls.iterator().next(); @@ -803,10 +812,11 @@ public class CallAudioManager extends CallsManagerListenerBase { mHoldingCalls.stream().map(c -> c.getId()).collect(Collectors.joining(",")) ); if (mForegroundCall != oldForegroundCall) { - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE); - if (mForegroundCall != null) { + if (mForegroundCall != null + && mFeatureFlags.ensureAudioModeUpdatesOnForegroundCallChange()) { // Ensure the voip audio mode for the new foreground call is taken into account. mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE, diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java index 9ad9094ef..71956a1e0 100644 --- a/src/com/android/server/telecom/CallAudioModeStateMachine.java +++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java @@ -16,6 +16,8 @@ package com.android.server.telecom; +import android.media.AudioAttributes; +import android.media.AudioFocusRequest; import android.media.AudioManager; import android.os.Looper; import android.os.Message; @@ -29,6 +31,7 @@ import com.android.internal.util.IState; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.State; import com.android.internal.util.StateMachine; +import com.android.server.telecom.flags.FeatureFlags; public class CallAudioModeStateMachine extends StateMachine { /** @@ -38,11 +41,29 @@ public class CallAudioModeStateMachine extends StateMachine { private LocalLog mLocalLog = new LocalLog(20); public static class Factory { public CallAudioModeStateMachine create(SystemStateHelper systemStateHelper, - AudioManager am) { - return new CallAudioModeStateMachine(systemStateHelper, am); + AudioManager am, FeatureFlags featureFlags, + CallAudioCommunicationDeviceTracker callAudioCommunicationDeviceTracker) { + return new CallAudioModeStateMachine(systemStateHelper, am, + featureFlags, callAudioCommunicationDeviceTracker); } } + private static final AudioAttributes RING_AUDIO_ATTRIBUTES = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) + .setLegacyStreamType(AudioManager.STREAM_RING) + .build(); + public static final AudioFocusRequest RING_AUDIO_FOCUS_REQUEST = new AudioFocusRequest + .Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) + .setAudioAttributes(RING_AUDIO_ATTRIBUTES).build(); + + private static final AudioAttributes CALL_AUDIO_ATTRIBUTES = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) + .setLegacyStreamType(AudioManager.STREAM_VOICE_CALL) + .build(); + public static final AudioFocusRequest CALL_AUDIO_FOCUS_REQUEST = new AudioFocusRequest + .Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) + .setAudioAttributes(CALL_AUDIO_ATTRIBUTES).build(); + public static class MessageArgs { public boolean hasActiveOrDialingCalls; public boolean hasRingingCalls; @@ -212,6 +233,8 @@ public class CallAudioModeStateMachine extends StateMachine { public static final String STREAMING_STATE_NAME = StreamingFocusState.class.getSimpleName(); public static final String COMMS_STATE_NAME = VoipCallFocusState.class.getSimpleName(); + private AudioFocusRequest mCurrentAudioFocusRequest = null; + private class BaseState extends State { @Override public boolean processMessage(Message msg) { @@ -256,8 +279,20 @@ public class CallAudioModeStateMachine extends StateMachine { Log.i(LOG_TAG, "Audio focus entering UNFOCUSED state"); mLocalLog.log("Enter UNFOCUSED"); if (mIsInitialized) { - mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.NO_FOCUS); - mAudioManager.setMode(AudioManager.MODE_NORMAL); + // Clear any communication device that was requested previously. + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { + mCommunicationDeviceTracker.clearCommunicationDevice(mCommunicationDeviceTracker + .getCurrentLocallyRequestedCommunicationDevice()); + } + if (mFeatureFlags.setAudioModeBeforeAbandonFocus()) { + mAudioManager.setMode(AudioManager.MODE_NORMAL); + mCallAudioManager.setCallAudioRouteFocusState( + CallAudioRouteStateMachine.NO_FOCUS); + } else { + mCallAudioManager.setCallAudioRouteFocusState( + CallAudioRouteStateMachine.NO_FOCUS); + mAudioManager.setMode(AudioManager.MODE_NORMAL); + } mLocalLog.log("Mode MODE_NORMAL"); mMostRecentMode = AudioManager.MODE_NORMAL; // Don't release focus here -- wait until we get a signal that any other audio @@ -310,7 +345,14 @@ public class CallAudioModeStateMachine extends StateMachine { return HANDLED; case AUDIO_OPERATIONS_COMPLETE: Log.i(LOG_TAG, "Abandoning audio focus: now UNFOCUSED"); - mAudioManager.abandonAudioFocusForCall(); + if (mFeatureFlags.telecomResolveHiddenDependencies()) { + if (mCurrentAudioFocusRequest != null) { + mAudioManager.abandonAudioFocusRequest(mCurrentAudioFocusRequest); + mCurrentAudioFocusRequest = null; + } + } else { + mAudioManager.abandonAudioFocusForCall(); + } return HANDLED; default: // The forced focus switch commands are handled by BaseState. @@ -381,7 +423,14 @@ public class CallAudioModeStateMachine extends StateMachine { return HANDLED; case AUDIO_OPERATIONS_COMPLETE: Log.i(LOG_TAG, "Abandoning audio focus: now AUDIO_PROCESSING"); - mAudioManager.abandonAudioFocusForCall(); + if (mFeatureFlags.telecomResolveHiddenDependencies()) { + if (mCurrentAudioFocusRequest != null) { + mAudioManager.abandonAudioFocusRequest(mCurrentAudioFocusRequest); + mCurrentAudioFocusRequest = null; + } + } else { + mAudioManager.abandonAudioFocusForCall(); + } return HANDLED; default: // The forced focus switch commands are handled by BaseState. @@ -406,8 +455,13 @@ public class CallAudioModeStateMachine extends StateMachine { } if (mCallAudioManager.startRinging()) { - mAudioManager.requestAudioFocusForCall( - AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + if (mFeatureFlags.telecomResolveHiddenDependencies()) { + mCurrentAudioFocusRequest = RING_AUDIO_FOCUS_REQUEST; + mAudioManager.requestAudioFocus(RING_AUDIO_FOCUS_REQUEST); + } else { + mAudioManager.requestAudioFocusForCall( + AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + } // Do not set MODE_RINGTONE if we were previously in the CALL_SCREENING mode -- // this trips up the audio system. if (mAudioManager.getMode() != AudioManager.MODE_CALL_SCREENING) { @@ -504,8 +558,13 @@ public class CallAudioModeStateMachine extends StateMachine { public void enter() { Log.i(LOG_TAG, "Audio focus entering SIM CALL state"); mLocalLog.log("Enter SIM_CALL"); - mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + if (mFeatureFlags.telecomResolveHiddenDependencies()) { + mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST; + mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST); + } else { + mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + } mAudioManager.setMode(AudioManager.MODE_IN_CALL); mLocalLog.log("Mode MODE_IN_CALL"); mMostRecentMode = AudioManager.MODE_IN_CALL; @@ -587,8 +646,13 @@ public class CallAudioModeStateMachine extends StateMachine { public void enter() { Log.i(LOG_TAG, "Audio focus entering VOIP CALL state"); mLocalLog.log("Enter VOIP_CALL"); - mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + if (mFeatureFlags.telecomResolveHiddenDependencies()) { + mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST; + mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST); + } else { + mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + } mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); mLocalLog.log("Mode MODE_IN_COMMUNICATION"); mMostRecentMode = AudioManager.MODE_IN_COMMUNICATION; @@ -670,12 +734,12 @@ public class CallAudioModeStateMachine extends StateMachine { mAudioManager.setMode(AudioManager.MODE_COMMUNICATION_REDIRECT); mMostRecentMode = AudioManager.MODE_NORMAL; mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS); - mCallAudioManager.getCallAudioRouteStateMachine().sendMessageWithSessionInfo( + mCallAudioManager.getCallAudioRouteAdapter().sendMessageWithSessionInfo( CallAudioRouteStateMachine.STREAMING_FORCE_ENABLED); } private void preExit() { - mCallAudioManager.getCallAudioRouteStateMachine().sendMessageWithSessionInfo( + mCallAudioManager.getCallAudioRouteAdapter().sendMessageWithSessionInfo( CallAudioRouteStateMachine.STREAMING_FORCE_DISABLED); } @@ -742,8 +806,13 @@ public class CallAudioModeStateMachine extends StateMachine { public void enter() { Log.i(LOG_TAG, "Audio focus entering TONE/HOLDING state"); mLocalLog.log("Enter TONE/HOLDING"); - mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + if (mFeatureFlags.telecomResolveHiddenDependencies()) { + mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST; + mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST); + } else { + mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + } mAudioManager.setMode(mMostRecentMode); mLocalLog.log("Mode " + mMostRecentMode); mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS); @@ -815,16 +884,21 @@ public class CallAudioModeStateMachine extends StateMachine { private final AudioManager mAudioManager; private final SystemStateHelper mSystemStateHelper; private CallAudioManager mCallAudioManager; + private FeatureFlags mFeatureFlags; + private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker; private int mMostRecentMode; private boolean mIsInitialized = false; public CallAudioModeStateMachine(SystemStateHelper systemStateHelper, - AudioManager audioManager) { + AudioManager audioManager, FeatureFlags featureFlags, + CallAudioCommunicationDeviceTracker callAudioCommunicationDeviceTracker) { super(CallAudioModeStateMachine.class.getSimpleName()); mAudioManager = audioManager; mSystemStateHelper = systemStateHelper; mMostRecentMode = AudioManager.MODE_NORMAL; + mFeatureFlags = featureFlags; + mCommunicationDeviceTracker = callAudioCommunicationDeviceTracker; createStates(); } @@ -833,11 +907,14 @@ public class CallAudioModeStateMachine extends StateMachine { * Used for testing */ public CallAudioModeStateMachine(SystemStateHelper systemStateHelper, - AudioManager audioManager, Looper looper) { + AudioManager audioManager, Looper looper, FeatureFlags featureFlags, + CallAudioCommunicationDeviceTracker communicationDeviceTracker) { super(CallAudioModeStateMachine.class.getSimpleName(), looper); mAudioManager = audioManager; mSystemStateHelper = systemStateHelper; mMostRecentMode = AudioManager.MODE_NORMAL; + mFeatureFlags = featureFlags; + mCommunicationDeviceTracker = communicationDeviceTracker; createStates(); } diff --git a/src/com/android/server/telecom/CallAudioRouteAdapter.java b/src/com/android/server/telecom/CallAudioRouteAdapter.java new file mode 100644 index 000000000..7f7b43c70 --- /dev/null +++ b/src/com/android/server/telecom/CallAudioRouteAdapter.java @@ -0,0 +1,19 @@ +package com.android.server.telecom; + +import android.os.Handler; +import android.telecom.CallAudioState; + +import com.android.internal.util.IndentingPrintWriter; + +public interface CallAudioRouteAdapter { + void initialize(); + void sendMessageWithSessionInfo(int message); + void sendMessageWithSessionInfo(int message, int arg); + void sendMessageWithSessionInfo(int message, int arg, String data); + void sendMessage(int message, Runnable r); + void setCallAudioManager(CallAudioManager callAudioManager); + CallAudioState getCurrentCallAudioState(); + boolean isHfpDeviceAvailable(); + Handler getAdapterHandler(); + void dump(IndentingPrintWriter pw); +} diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java new file mode 100644 index 000000000..f8c49bb82 --- /dev/null +++ b/src/com/android/server/telecom/CallAudioRouteController.java @@ -0,0 +1,64 @@ +package com.android.server.telecom; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.telecom.CallAudioState; + +import com.android.internal.util.IndentingPrintWriter; + +public class CallAudioRouteController implements CallAudioRouteAdapter { + private Handler mHandler; + + public CallAudioRouteController() { + HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName()); + handlerThread.start(); + mHandler = new Handler(handlerThread.getLooper()); + } + @Override + public void initialize() { + } + + @Override + public void sendMessageWithSessionInfo(int message) { + } + + @Override + public void sendMessageWithSessionInfo(int message, int arg) { + + } + + @Override + public void sendMessageWithSessionInfo(int message, int arg, String data) { + + } + + @Override + public void sendMessage(int message, Runnable r) { + + } + + @Override + public void setCallAudioManager(CallAudioManager callAudioManager) { + } + + @Override + public CallAudioState getCurrentCallAudioState() { + return null; + } + + @Override + public boolean isHfpDeviceAvailable() { + return false; + } + + @Override + public Handler getAdapterHandler() { + return mHandler; + } + + @Override + public void dump(IndentingPrintWriter pw) { + + } +} diff --git a/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java index af0757c15..8a87c2236 100644 --- a/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java +++ b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java @@ -25,17 +25,17 @@ import com.android.server.telecom.bluetooth.BluetoothRouteManager; public class CallAudioRoutePeripheralAdapter implements WiredHeadsetManager.Listener, DockManager.Listener, BluetoothRouteManager.BluetoothStateListener { - private final CallAudioRouteStateMachine mCallAudioRouteStateMachine; + private final CallAudioRouteAdapter mCallAudioAdapter; private final BluetoothRouteManager mBluetoothRouteManager; private final AsyncRingtonePlayer mRingtonePlayer; public CallAudioRoutePeripheralAdapter( - CallAudioRouteStateMachine callAudioRouteStateMachine, + CallAudioRouteAdapter callAudioRouteAdapter, BluetoothRouteManager bluetoothManager, WiredHeadsetManager wiredHeadsetManager, DockManager dockManager, AsyncRingtonePlayer ringtonePlayer) { - mCallAudioRouteStateMachine = callAudioRouteStateMachine; + mCallAudioAdapter = callAudioRouteAdapter; mBluetoothRouteManager = bluetoothManager; mRingtonePlayer = ringtonePlayer; @@ -60,26 +60,26 @@ public class CallAudioRoutePeripheralAdapter implements WiredHeadsetManager.List @Override public void onBluetoothDeviceListChanged() { - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.BLUETOOTH_DEVICE_LIST_CHANGED); } @Override public void onBluetoothActiveDevicePresent() { - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_PRESENT); } @Override public void onBluetoothActiveDeviceGone() { - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_GONE); } @Override public void onBluetoothAudioConnected() { mRingtonePlayer.updateBtActiveState(true); - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.BT_AUDIO_CONNECTED); } @@ -87,20 +87,20 @@ public class CallAudioRoutePeripheralAdapter implements WiredHeadsetManager.List public void onBluetoothAudioConnecting() { mRingtonePlayer.updateBtActiveState(false); // Pretend like audio is connected when communicating w/ CARSM. - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.BT_AUDIO_CONNECTED); } @Override public void onBluetoothAudioDisconnected() { mRingtonePlayer.updateBtActiveState(false); - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.BT_AUDIO_DISCONNECTED); } @Override public void onUnexpectedBluetoothStateChange() { - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE); } @@ -111,17 +111,17 @@ public class CallAudioRoutePeripheralAdapter implements WiredHeadsetManager.List @Override public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) { if (!oldIsPluggedIn && newIsPluggedIn) { - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET); } else if (oldIsPluggedIn && !newIsPluggedIn){ - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET); } } @Override public void onDockChanged(boolean isDocked) { - mCallAudioRouteStateMachine.sendMessageWithSessionInfo( + mCallAudioAdapter.sendMessageWithSessionInfo( isDocked ? CallAudioRouteStateMachine.CONNECT_DOCK : CallAudioRouteStateMachine.DISCONNECT_DOCK ); diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java index db84d6e74..c0bb50e2f 100644 --- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java +++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java @@ -28,6 +28,7 @@ import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.IAudioService; import android.os.Binder; +import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.RemoteException; @@ -44,6 +45,7 @@ import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.server.telecom.bluetooth.BluetoothRouteManager; +import com.android.server.telecom.flags.FeatureFlags; import java.util.Collection; import java.util.HashMap; @@ -72,7 +74,7 @@ import java.util.concurrent.Executor; * from a wired headset * mIsMuted: a boolean indicating whether the audio is muted */ -public class CallAudioRouteStateMachine extends StateMachine { +public class CallAudioRouteStateMachine extends StateMachine implements CallAudioRouteAdapter { public static class Factory { public CallAudioRouteStateMachine create( @@ -84,7 +86,8 @@ public class CallAudioRouteStateMachine extends StateMachine { CallAudioManager.AudioServiceFactory audioServiceFactory, int earpieceControl, Executor asyncTaskExecutor, - CallAudioCommunicationDeviceTracker communicationDeviceTracker) { + CallAudioCommunicationDeviceTracker communicationDeviceTracker, + FeatureFlags featureFlags) { return new CallAudioRouteStateMachine(context, callsManager, bluetoothManager, @@ -93,7 +96,8 @@ public class CallAudioRouteStateMachine extends StateMachine { audioServiceFactory, earpieceControl, asyncTaskExecutor, - communicationDeviceTracker); + communicationDeviceTracker, + featureFlags); } } /** Values for CallAudioRouteStateMachine constructor's earPieceRouting arg. */ @@ -373,8 +377,10 @@ public class CallAudioRouteStateMachine extends StateMachine { public void enter() { super.enter(); setSpeakerphoneOn(false); - mCommunicationDeviceTracker.setCommunicationDevice( - AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, null); + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { + mCommunicationDeviceTracker.setCommunicationDevice( + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, null); + } CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_EARPIECE, mAvailableRoutes, null, mBluetoothRouteManager.getConnectedDevices()); @@ -405,8 +411,10 @@ public class CallAudioRouteStateMachine extends StateMachine { case SWITCH_BLUETOOTH: case USER_SWITCH_BLUETOOTH: if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) { - mCommunicationDeviceTracker.clearCommunicationDevice( - AudioDeviceInfo.TYPE_BUILTIN_EARPIECE); + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { + mCommunicationDeviceTracker.clearCommunicationDevice( + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE); + } if (mAudioFocusType == ACTIVE_FOCUS || mBluetoothRouteManager.isInbandRingingEnabled()) { String address = (msg.obj instanceof SomeArgs) ? @@ -423,8 +431,10 @@ public class CallAudioRouteStateMachine extends StateMachine { case SWITCH_HEADSET: case USER_SWITCH_HEADSET: if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) { - mCommunicationDeviceTracker.clearCommunicationDevice( - AudioDeviceInfo.TYPE_BUILTIN_EARPIECE); + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { + mCommunicationDeviceTracker.clearCommunicationDevice( + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE); + } transitionTo(mActiveHeadsetRoute); } else { Log.w(this, "Ignoring switch to headset command. Not available."); @@ -434,8 +444,10 @@ public class CallAudioRouteStateMachine extends StateMachine { // fall through; we want to switch to speaker mode when docked and in a call. case SWITCH_SPEAKER: case USER_SWITCH_SPEAKER: - mCommunicationDeviceTracker.clearCommunicationDevice( - AudioDeviceInfo.TYPE_BUILTIN_EARPIECE); + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { + mCommunicationDeviceTracker.clearCommunicationDevice( + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE); + } setSpeakerphoneOn(true); // fall through case SPEAKER_ON: @@ -589,8 +601,10 @@ public class CallAudioRouteStateMachine extends StateMachine { public void enter() { super.enter(); setSpeakerphoneOn(false); - mCommunicationDeviceTracker.setCommunicationDevice( - AudioDeviceInfo.TYPE_WIRED_HEADSET, null); + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { + mCommunicationDeviceTracker.setCommunicationDevice( + AudioDeviceInfo.TYPE_WIRED_HEADSET, null); + } CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_WIRED_HEADSET, mAvailableRoutes, null, mBluetoothRouteManager.getConnectedDevices()); setSystemAudioState(newState, true); @@ -612,8 +626,10 @@ public class CallAudioRouteStateMachine extends StateMachine { case SWITCH_EARPIECE: case USER_SWITCH_EARPIECE: if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) { - mCommunicationDeviceTracker.clearCommunicationDevice( - AudioDeviceInfo.TYPE_WIRED_HEADSET); + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { + mCommunicationDeviceTracker.clearCommunicationDevice( + AudioDeviceInfo.TYPE_WIRED_HEADSET); + } transitionTo(mActiveEarpieceRoute); } else { Log.w(this, "Ignoring switch to earpiece command. Not available."); @@ -629,8 +645,10 @@ public class CallAudioRouteStateMachine extends StateMachine { || mBluetoothRouteManager.isInbandRingingEnabled()) { String address = (msg.obj instanceof SomeArgs) ? (String) ((SomeArgs) msg.obj).arg2 : null; - mCommunicationDeviceTracker.clearCommunicationDevice( - AudioDeviceInfo.TYPE_WIRED_HEADSET); + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { + mCommunicationDeviceTracker.clearCommunicationDevice( + AudioDeviceInfo.TYPE_WIRED_HEADSET); + } // Omit transition to ActiveBluetoothRoute until actual connection. setBluetoothOn(address); } else { @@ -647,8 +665,10 @@ public class CallAudioRouteStateMachine extends StateMachine { return HANDLED; case SWITCH_SPEAKER: case USER_SWITCH_SPEAKER: - mCommunicationDeviceTracker.clearCommunicationDevice( - AudioDeviceInfo.TYPE_WIRED_HEADSET); + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { + mCommunicationDeviceTracker.clearCommunicationDevice( + AudioDeviceInfo.TYPE_WIRED_HEADSET); + } setSpeakerphoneOn(true); // fall through case SPEAKER_ON: @@ -816,7 +836,12 @@ public class CallAudioRouteStateMachine extends StateMachine { // the BT connection fails to be set. Previously, the logic was to setBluetoothOn in // ACTIVE_FOCUS but the route would still remain in a quiescent route, so instead we // should be transitioning directly into the active route. - setBluetoothOn(null); + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { + setBluetoothOn(null); + } + if (mFeatureFlags.updateRouteMaskWhenBtConnected()) { + mAvailableRoutes |= ROUTE_BLUETOOTH; + } CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH, mAvailableRoutes, mBluetoothRouteManager.getBluetoothAudioConnectedDevice(), mBluetoothRouteManager.getConnectedDevices()); @@ -917,8 +942,13 @@ public class CallAudioRouteStateMachine extends StateMachine { case SWITCH_FOCUS: if (msg.arg1 == NO_FOCUS) { // Only disconnect audio here instead of routing away from BT entirely. - mBluetoothRouteManager.disconnectAudio(); - transitionTo(mQuiescentBluetoothRoute); + if (mFeatureFlags.transitRouteBeforeAudioDisconnectBt()) { + transitionTo(mQuiescentBluetoothRoute); + mBluetoothRouteManager.disconnectAudio(); + } else { + mBluetoothRouteManager.disconnectAudio(); + reinitialize(); + } mCallAudioManager.notifyAudioOperationsComplete(); } else if (msg.arg1 == RINGING_FOCUS && !mBluetoothRouteManager.isInbandRingingEnabled()) { @@ -1043,6 +1073,9 @@ public class CallAudioRouteStateMachine extends StateMachine { public void enter() { super.enter(); mHasUserExplicitlyLeftBluetooth = false; + if (mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()) { + setMuteOn(false); + } updateInternalCallAudioState(); } @@ -1091,7 +1124,11 @@ public class CallAudioRouteStateMachine extends StateMachine { if (msg.arg1 == ACTIVE_FOCUS) { // It is possible that the connection to BT will fail while in-call, in // which case, we want to transition into the active route. - transitionTo(mActiveBluetoothRoute); + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { + transitionTo(mActiveBluetoothRoute); + } else { + setBluetoothOn(null); + } } else if (msg.arg1 == RINGING_FOCUS) { if (mBluetoothRouteManager.isInbandRingingEnabled()) { setBluetoothOn(null); @@ -1241,7 +1278,13 @@ public class CallAudioRouteStateMachine extends StateMachine { // Expected, since we just transitioned here return HANDLED; case SPEAKER_OFF: - sendInternalMessage(SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE); + // Check if we already requested to connect to other devices and just waiting + // for their response. In some cases, this SPEAKER_OFF message may come in + // before the response, we can just ignore the message here to not re-evaluate + // the baseline route incorrectly + if (!mBluetoothRouteManager.isBluetoothAudioConnectedOrPending()) { + sendInternalMessage(SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE); + } return HANDLED; case SWITCH_FOCUS: if (msg.arg1 == NO_FOCUS) { @@ -1541,6 +1584,7 @@ public class CallAudioRouteStateMachine extends StateMachine { private CallAudioManager mCallAudioManager; private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker; + private FeatureFlags mFeatureFlags; public CallAudioRouteStateMachine( Context context, @@ -1551,7 +1595,8 @@ public class CallAudioRouteStateMachine extends StateMachine { CallAudioManager.AudioServiceFactory audioServiceFactory, int earpieceControl, Executor asyncTaskExecutor, - CallAudioCommunicationDeviceTracker communicationDeviceTracker) { + CallAudioCommunicationDeviceTracker communicationDeviceTracker, + FeatureFlags featureFlags) { super(NAME); mContext = context; mCallsManager = callsManager; @@ -1563,6 +1608,7 @@ public class CallAudioRouteStateMachine extends StateMachine { mLock = callsManager.getLock(); mAsyncTaskExecutor = asyncTaskExecutor; mCommunicationDeviceTracker = communicationDeviceTracker; + mFeatureFlags = featureFlags; createStates(earpieceControl); } @@ -1575,7 +1621,8 @@ public class CallAudioRouteStateMachine extends StateMachine { StatusBarNotifier statusBarNotifier, CallAudioManager.AudioServiceFactory audioServiceFactory, int earpieceControl, Looper looper, Executor asyncTaskExecutor, - CallAudioCommunicationDeviceTracker communicationDeviceTracker) { + CallAudioCommunicationDeviceTracker communicationDeviceTracker, + FeatureFlags featureFlags) { super(NAME, looper); mContext = context; mCallsManager = callsManager; @@ -1587,7 +1634,7 @@ public class CallAudioRouteStateMachine extends StateMachine { mLock = callsManager.getLock(); mAsyncTaskExecutor = asyncTaskExecutor; mCommunicationDeviceTracker = communicationDeviceTracker; - + mFeatureFlags = featureFlags; createStates(earpieceControl); } @@ -1704,6 +1751,11 @@ public class CallAudioRouteStateMachine extends StateMachine { sendMessage(message, arg, 0, args); } + @Override + public void sendMessage(int message, Runnable r) { + super.sendMessage(message, r); + } + /** * This is for state-independent changes in audio route (i.e. muting or runnables) * @param msg that couldn't be handled. @@ -1733,9 +1785,19 @@ public class CallAudioRouteStateMachine extends StateMachine { } return; case UPDATE_SYSTEM_AUDIO_ROUTE: - updateInternalCallAudioState(); - updateRouteForForegroundCall(); - resendSystemAudioState(); + if (mFeatureFlags.availableRoutesNeverUpdatedAfterSetSystemAudioState()) { + // Ensure available routes is updated. + updateRouteForForegroundCall(); + // Ensure current audio state gets updated to take this into account. + updateInternalCallAudioState(); + // Either resend the current audio state as it stands, or update to reflect any + // changes put into place based on mAvailableRoutes + setSystemAudioState(mCurrentCallAudioState, true); + } else { + updateInternalCallAudioState(); + updateRouteForForegroundCall(); + resendSystemAudioState(); + } return; case RUN_RUNNABLE: java.lang.Runnable r = (java.lang.Runnable) msg.obj; @@ -1760,7 +1822,7 @@ public class CallAudioRouteStateMachine extends StateMachine { } public void dumpPendingMessages(IndentingPrintWriter pw) { - getHandler().getLooper().dump(pw::println, ""); + getAdapterHandler().getLooper().dump(pw::println, ""); } public boolean isHfpDeviceAvailable() { @@ -1772,8 +1834,8 @@ public class CallAudioRouteStateMachine extends StateMachine { final boolean hasAnyCalls = mCallsManager.hasAnyCalls(); // These APIs are all via two-way binder calls so can potentially block Telecom. Since none // of this has to happen in the Telecom lock we'll offload it to the async executor. - mAsyncTaskExecutor.execute(() -> { - boolean speakerOn = false; + boolean speakerOn = false; + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { if (on) { speakerOn = mCommunicationDeviceTracker.setCommunicationDevice( AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, null); @@ -1781,8 +1843,10 @@ public class CallAudioRouteStateMachine extends StateMachine { mCommunicationDeviceTracker.clearCommunicationDevice( AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); } - mStatusBarNotifier.notifySpeakerphone(hasAnyCalls && speakerOn); - }); + } else { + speakerOn = processLegacySpeakerCommunicationDevice(on); + } + mStatusBarNotifier.notifySpeakerphone(hasAnyCalls && speakerOn); } private void setBluetoothOn(String address) { @@ -1880,6 +1944,11 @@ public class CallAudioRouteStateMachine extends StateMachine { setSystemAudioState(mLastKnownCallAudioState, true); } + @VisibleForTesting + public CallAudioState getLastKnownCallAudioState() { + return mLastKnownCallAudioState; + } + private void setSystemAudioState(CallAudioState newCallAudioState, boolean force) { synchronized (mLock) { Log.i(this, "setSystemAudioState: changing from %s to %s", mLastKnownCallAudioState, @@ -1997,6 +2066,58 @@ public class CallAudioRouteStateMachine extends StateMachine { return false; } + private boolean isWatchActiveOrOnlyWatchesAvailable() { + if (!mFeatureFlags.ignoreAutoRouteToWatchDevice()) { + Log.i(this, "isWatchActiveOrOnlyWatchesAvailable: Flag is disabled."); + return false; + } + + boolean containsWatchDevice = false; + boolean containsNonWatchDevice = false; + Collection<BluetoothDevice> connectedBtDevices = + mBluetoothRouteManager.getConnectedDevices(); + + for (BluetoothDevice connectedDevice: connectedBtDevices) { + if (mBluetoothRouteManager.isWatch(connectedDevice)) { + containsWatchDevice = true; + } else { + containsNonWatchDevice = true; + } + } + + // Don't ignore switch if watch is already the active device. + boolean isActiveDeviceWatch = mBluetoothRouteManager.isWatch( + mBluetoothRouteManager.getBluetoothAudioConnectedDevice()); + Log.i(this, "isWatchActiveOrOnlyWatchesAvailable: contains watch: %s, contains " + + "non-wearable device: %s, is active device a watch: %s.", + containsWatchDevice, containsNonWatchDevice, isActiveDeviceWatch); + return containsWatchDevice && !containsNonWatchDevice && !isActiveDeviceWatch; + } + + private boolean processLegacySpeakerCommunicationDevice(boolean on) { + AudioDeviceInfo speakerDevice = null; + for (AudioDeviceInfo info : mAudioManager.getAvailableCommunicationDevices()) { + if (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { + speakerDevice = info; + break; + } + } + boolean speakerOn = false; + if (speakerDevice != null && on) { + boolean result = mAudioManager.setCommunicationDevice(speakerDevice); + if (result) { + speakerOn = true; + } + } else { + AudioDeviceInfo curDevice = mAudioManager.getCommunicationDevice(); + if (curDevice != null + && curDevice.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { + mAudioManager.clearCommunicationDevice(); + } + } + return speakerOn; + } + private int calculateBaselineRouteMessage(boolean isExplicitUserRequest, boolean includeBluetooth) { boolean isSkipEarpiece = false; @@ -2009,7 +2130,7 @@ public class CallAudioRouteStateMachine extends StateMachine { } if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0 && !mHasUserExplicitlyLeftBluetooth - && includeBluetooth) { + && includeBluetooth && !isWatchActiveOrOnlyWatchesAvailable()) { return isExplicitUserRequest ? USER_SWITCH_BLUETOOTH : SWITCH_BLUETOOTH; } else if ((mAvailableRoutes & ROUTE_EARPIECE) != 0 && !isSkipEarpiece) { return isExplicitUserRequest ? USER_SWITCH_EARPIECE : SWITCH_EARPIECE; @@ -2065,4 +2186,8 @@ public class CallAudioRouteStateMachine extends StateMachine { return base; } + + public Handler getAdapterHandler() { + return getHandler(); + } } diff --git a/src/com/android/server/telecom/CallEndpointController.java b/src/com/android/server/telecom/CallEndpointController.java index 7e11b47f4..4738cd49f 100644 --- a/src/com/android/server/telecom/CallEndpointController.java +++ b/src/com/android/server/telecom/CallEndpointController.java @@ -87,7 +87,7 @@ public class CallEndpointController extends CallsManagerListenerBase { } public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) { - Log.d(this, "requestCallEndpointChange %s", endpoint); + Log.i(this, "requestCallEndpointChange %s", endpoint); int route = mTypeToRouteMap.get(endpoint.getEndpointType()); String bluetoothAddress = getBluetoothAddress(endpoint); @@ -99,7 +99,6 @@ public class CallEndpointController extends CallsManagerListenerBase { } if (isCurrentEndpointRequestedEndpoint(route, bluetoothAddress)) { - Log.d(this, "requestCallEndpointChange: requested endpoint is already active"); callback.send(CallEndpoint.ENDPOINT_OPERATION_SUCCESS, new Bundle()); return; } @@ -130,21 +129,27 @@ public class CallEndpointController extends CallsManagerListenerBase { return false; } CallAudioState currentAudioState = mCallsManager.getCallAudioManager().getCallAudioState(); - // requested non-bt endpoint is already active - if (requestedRoute != CallAudioState.ROUTE_BLUETOOTH && - requestedRoute == currentAudioState.getRoute()) { - return true; - } - // requested bt endpoint is already active - if (requestedRoute == CallAudioState.ROUTE_BLUETOOTH && - currentAudioState.getActiveBluetoothDevice() != null && - requestedAddress.equals( - currentAudioState.getActiveBluetoothDevice().getAddress())) { - return true; + if (requestedRoute == currentAudioState.getRoute()) { + if (requestedRoute != CallAudioState.ROUTE_BLUETOOTH) { + // The audio route (earpiece, speaker, etc.) is already active + // and Telecom can ignore the spam request! + Log.i(this, "iCERE: user requested a non-BT route that is already active"); + return true; + } else if (hasSameBluetoothAddress(currentAudioState, requestedAddress)) { + // if the requested (BT route, device) is active, ignore the request... + Log.i(this, "iCERE: user requested a BT endpoint that is already active"); + return true; + } } return false; } + public boolean hasSameBluetoothAddress(CallAudioState audioState, String requestedAddress) { + boolean hasActiveBtDevice = audioState.getActiveBluetoothDevice() != null; + return hasActiveBtDevice && requestedAddress.equals( + audioState.getActiveBluetoothDevice().getAddress()); + } + private Bundle getErrorResult(int result) { String message; int resultCode; diff --git a/src/com/android/server/telecom/CallIntentProcessor.java b/src/com/android/server/telecom/CallIntentProcessor.java index 7953324df..062c872dc 100644 --- a/src/com/android/server/telecom/CallIntentProcessor.java +++ b/src/com/android/server/telecom/CallIntentProcessor.java @@ -1,6 +1,7 @@ package com.android.server.telecom; import com.android.server.telecom.components.ErrorDialogActivity; +import com.android.server.telecom.flags.FeatureFlags; import android.content.Context; import android.content.Intent; @@ -32,7 +33,7 @@ import java.util.concurrent.CompletableFuture; public class CallIntentProcessor { public interface Adapter { void processOutgoingCallIntent(Context context, CallsManager callsManager, - Intent intent, String callingPackage); + Intent intent, String callingPackage, FeatureFlags featureFlags); void processIncomingCallIntent(CallsManager callsManager, Intent intent); void processUnknownCallIntent(CallsManager callsManager, Intent intent); } @@ -45,9 +46,9 @@ public class CallIntentProcessor { @Override public void processOutgoingCallIntent(Context context, CallsManager callsManager, - Intent intent, String callingPackage) { + Intent intent, String callingPackage, FeatureFlags featureFlags) { CallIntentProcessor.processOutgoingCallIntent(context, callsManager, intent, - callingPackage, mDefaultDialerCache); + callingPackage, mDefaultDialerCache, featureFlags); } @Override @@ -73,12 +74,14 @@ public class CallIntentProcessor { private final Context mContext; private final CallsManager mCallsManager; private final DefaultDialerCache mDefaultDialerCache; + private final FeatureFlags mFeatureFlags; public CallIntentProcessor(Context context, CallsManager callsManager, - DefaultDialerCache defaultDialerCache) { + DefaultDialerCache defaultDialerCache, FeatureFlags featureFlags) { this.mContext = context; this.mCallsManager = callsManager; this.mDefaultDialerCache = defaultDialerCache; + this.mFeatureFlags = featureFlags; } public void processIntent(Intent intent, String callingPackage) { @@ -90,7 +93,7 @@ public class CallIntentProcessor { processUnknownCallIntent(mCallsManager, intent); } else { processOutgoingCallIntent(mContext, mCallsManager, intent, callingPackage, - mDefaultDialerCache); + mDefaultDialerCache, mFeatureFlags); } Trace.endSection(); } @@ -107,7 +110,8 @@ public class CallIntentProcessor { CallsManager callsManager, Intent intent, String callingPackage, - DefaultDialerCache defaultDialerCache) { + DefaultDialerCache defaultDialerCache, + FeatureFlags featureFlags) { Uri handle = intent.getData(); String scheme = handle.getScheme(); @@ -182,10 +186,9 @@ public class CallIntentProcessor { boolean isPrivilegedDialer = defaultDialerCache.isDefaultOrSystemDialer(callingPackage, initiatingUser.getIdentifier()); - NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster( context, callsManager, intent, callsManager.getPhoneNumberUtilsAdapter(), - isPrivilegedDialer, defaultDialerCache, new MmiUtils()); + isPrivilegedDialer, defaultDialerCache, new MmiUtils(), featureFlags); // If the broadcaster comes back with an immediate error, disconnect and show a dialog. NewOutgoingCallIntentBroadcaster.CallDisposition disposition = broadcaster.evaluateCall(); diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java index 3005656b3..fc4e05d69 100644 --- a/src/com/android/server/telecom/CallLogManager.java +++ b/src/com/android/server/telecom/CallLogManager.java @@ -19,9 +19,13 @@ package com.android.server.telecom; import static android.provider.CallLog.Calls.BLOCK_REASON_NOT_BLOCKED; import static android.telephony.CarrierConfigManager.KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; import android.location.Country; import android.location.CountryDetector; import android.location.Location; @@ -30,6 +34,7 @@ import android.os.AsyncTask; import android.os.Looper; import android.os.UserHandle; import android.os.PersistableBundle; +import android.os.UserManager; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.telecom.Connection; @@ -42,13 +47,16 @@ import android.telecom.VideoProfile; import android.telephony.CarrierConfigManager; import android.telephony.PhoneNumberUtils; import android.telephony.SubscriptionManager; +import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; import com.android.server.telecom.callfiltering.CallFilteringResult; +import com.android.server.telecom.flags.FeatureFlags; import java.util.Arrays; import java.util.Locale; import java.util.Objects; +import java.util.UUID; import java.util.stream.Stream; /** @@ -68,16 +76,19 @@ public final class CallLogManager extends CallsManagerListenerBase { */ private static class AddCallArgs { public AddCallArgs(Context context, CallLog.AddCallParams params, - @Nullable LogCallCompletedListener logCallCompletedListener) { + @Nullable LogCallCompletedListener logCallCompletedListener, + @NonNull Call call) { this.context = context; this.params = params; this.logCallCompletedListener = logCallCompletedListener; + this.call = call; } // Since the members are accessed directly, we don't use the // mXxxx notation. public final Context context; public final CallLog.AddCallParams params; + public final Call call; @Nullable public final LogCallCompletedListener logCallCompletedListener; } @@ -88,29 +99,39 @@ public final class CallLogManager extends CallsManagerListenerBase { // TODO: come up with a better way to indicate in a android.telecom.DisconnectCause that // a conference was merged successfully private static final String REASON_IMS_MERGED_SUCCESSFULLY = "IMS_MERGED_SUCCESSFULLY"; + private static final UUID LOG_CALL_FAILED_ANOMALY_ID = + UUID.fromString("d9b38771-ff36-417b-8723-2363a870c702"); + private static final String LOG_CALL_FAILED_ANOMALY_DESC = + "Based on the current user, Telecom detected failure to record a call to the call log."; private final Context mContext; private final CarrierConfigManager mCarrierConfigManager; private final PhoneAccountRegistrar mPhoneAccountRegistrar; private final MissedCallNotifier mMissedCallNotifier; + private AnomalyReporterAdapter mAnomalyReporterAdapter; private static final String ACTION_CALLS_TABLE_ADD_ENTRY = - "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY"; + "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY"; private static final String PERMISSION_PROCESS_CALLLOG_INFO = - "android.permission.PROCESS_CALLLOG_INFO"; + "android.permission.PROCESS_CALLLOG_INFO"; private static final String CALL_TYPE = "callType"; private static final String CALL_DURATION = "duration"; private Object mLock; private String mCurrentCountryIso; + private final FeatureFlags mFeatureFlags; + public CallLogManager(Context context, PhoneAccountRegistrar phoneAccountRegistrar, - MissedCallNotifier missedCallNotifier) { + MissedCallNotifier missedCallNotifier, AnomalyReporterAdapter anomalyReporterAdapter, + FeatureFlags featureFlags) { mContext = context; mCarrierConfigManager = (CarrierConfigManager) mContext .getSystemService(Context.CARRIER_CONFIG_SERVICE); mPhoneAccountRegistrar = phoneAccountRegistrar; mMissedCallNotifier = missedCallNotifier; + mAnomalyReporterAdapter = anomalyReporterAdapter; mLock = new Object(); + mFeatureFlags = featureFlags; } @Override @@ -149,9 +170,10 @@ public final class CallLogManager extends CallsManagerListenerBase { * Call was NOT in the "choose account" phase when disconnected * Call is NOT a conference call which had children (unless it was remotely hosted). * Call is NOT a child call from a conference which was remotely hosted. + * Call has NOT indicated it should be skipped for logging in its extras * Call is NOT simulating a single party conference. * Call was NOT explicitly canceled, except for disconnecting from a conference. - * Call is NOT an external call + * Call is NOT an external call or an external call on watch. * Call is NOT disconnected because of merging into a conference. * Call is NOT a self-managed call OR call is a self-managed call which has indicated it * should be logged in its PhoneAccount @@ -180,6 +202,11 @@ public final class CallLogManager extends CallsManagerListenerBase { return false; } + if (mFeatureFlags.telecomSkipLogBasedOnExtra() && call.getExtras() != null + && call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL)) { + return false; + } + // A child call of a conference which was remotely hosted; these didn't originate on this // device and should not be logged. if (call.getParentCall() != null && call.hasProperty(Connection.PROPERTY_REMOTELY_HOSTED)) { @@ -200,8 +227,10 @@ public final class CallLogManager extends CallsManagerListenerBase { & Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE) == Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE; } - // An external call - if (call.isExternalCall()) { + // An external and non-watch call + if (call.isExternalCall() && (!mContext.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_WATCH) + || !mFeatureFlags.telecomLogExternalWearableCalls())) { return false; } @@ -240,8 +269,13 @@ public final class CallLogManager extends CallsManagerListenerBase { logCall(call, type, new LogCallCompletedListener() { @Override public void onLogCompleted(@Nullable Uri uri) { - mMissedCallNotifier.showMissedCallNotification( - new MissedCallNotifier.CallInfo(call)); + if (mFeatureFlags.addCallUriForMissedCalls()){ + mMissedCallNotifier.showMissedCallNotification( + new MissedCallNotifier.CallInfo(call), uri); + } else { + mMissedCallNotifier.showMissedCallNotification( + new MissedCallNotifier.CallInfo(call), /* uri= */ null); + } } }, result); } else { @@ -263,7 +297,7 @@ public final class CallLogManager extends CallsManagerListenerBase { * {@link android.provider.CallLog.Calls#BLOCKED_TYPE}. */ void logCall(Call call, int callLogType, - @Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result) { + @Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result) { CallLog.AddCallParams.AddCallParametersBuilder paramBuilder = new CallLog.AddCallParams.AddCallParametersBuilder(); @@ -385,7 +419,7 @@ public final class CallLogManager extends CallsManagerListenerBase { okayToLogCall(accountHandle, logNumber, call.isEmergencyCall()); if (okayToLog) { AddCallArgs args = new AddCallArgs(mContext, paramBuilder.build(), - logCallCompletedListener); + logCallCompletedListener, call); Log.addEvent(call, LogUtils.Events.LOG_CALL, "number=" + Log.piiHandle(logNumber) + ",postDial=" + Log.piiHandle(call.getPostDialDigits()) + ",pres=" + call.getHandlePresentation()); @@ -516,8 +550,25 @@ public final class CallLogManager extends CallsManagerListenerBase { AddCallArgs c = callList[i]; mListeners[i] = c.logCallCompletedListener; try { - // May block. + Pair<Integer, Integer> startStats = getCallLogStats(c.call); + Log.i(TAG, "LogCall; about to log callId=%s, " + + "startCount=%d, startMaxId=%d", + c.call.getId(), startStats.first, startStats.second); + result[i] = Calls.addCall(c.context, c.params); + Pair<Integer, Integer> endStats = getCallLogStats(c.call); + Log.i(TAG, "LogCall; logged callId=%s, uri=%s, " + + "endCount=%d, endMaxId=%s", + c.call.getId(), result, endStats.first, endStats.second); + if ((endStats.second - startStats.second) <= 0) { + // No call was added or even worse we lost a call in the log. Trigger an + // anomaly report. Note: it technically possible that an app modified the + // call log while we were writing to it here; that is pretty unlikely, and + // the goal here is to try and identify potential anomalous conditions with + // logging calls. + mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID, + LOG_CALL_FAILED_ANOMALY_DESC); + } } catch (Exception e) { // This is very rare but may happen in legitimate cases. // E.g. If the phone is encrypted and thus write request fails, it may cause @@ -526,8 +577,10 @@ public final class CallLogManager extends CallsManagerListenerBase { // // We don't want to crash the whole process just because of that, so just log // it instead. - Log.e(TAG, e, "Exception raised during adding CallLog entry."); + Log.e(TAG, e, "LogCall: Exception raised adding callId=%s", c.call.getId()); result[i] = null; + mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID, + LOG_CALL_FAILED_ANOMALY_DESC); } } return result; @@ -602,4 +655,56 @@ public final class CallLogManager extends CallsManagerListenerBase { return mCurrentCountryIso; } } + + + /** + * Returns a pair containing the number of rows in the call log, as well as the maximum call log + * ID. There is a limit of 500 entries in the call log for a phone account, so once we hit 500 + * we can reasonably expect that number to not change before and after logging a call. + * We determine the maximum ID in the call log since this is a way we can objectively check if + * the provider did record a call log entry or not. Ideally there should be more call log + * entries after logging than before, and certainly not less. + * @return pair with number of rows in the call log and max id. + */ + private Pair<Integer, Integer> getCallLogStats(@NonNull Call call) { + try { + // Ensure we query the call log based on the current user. + final Context currentUserContext = mContext.createContextAsUser( + call.getAssociatedUser(), /* flags= */ 0); + final ContentResolver currentUserResolver = currentUserContext.getContentResolver(); + final UserManager userManager = currentUserContext.getSystemService(UserManager.class); + final int currentUserId = userManager.getProcessUserId(); + + // Use shadow provider based on current user unlock state. + Uri providerUri; + if (userManager.isUserUnlocked(currentUserId)) { + providerUri = Calls.CONTENT_URI; + } else { + providerUri = Calls.SHADOW_CONTENT_URI; + } + int maxCallId = -1; + int numFound; + try (Cursor countCursor = currentUserResolver.query(providerUri, + new String[]{Calls._ID}, + null, + null, + Calls._ID + " DESC")) { + numFound = countCursor.getCount(); + if (numFound > 0) { + countCursor.moveToFirst(); + maxCallId = countCursor.getInt(0); + } + } + return new Pair<>(numFound, maxCallId); + } catch (Exception e) { + // Oh jeepers, we crashed getting the call count. + Log.e(TAG, e, "getCountOfCallLogRows: failed"); + return new Pair<>(-1, -1); + } + } + + @VisibleForTesting + public void setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter){ + mAnomalyReporterAdapter = anomalyReporterAdapter; + } } diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java index f38d8a188..5db59c4af 100644..100755 --- a/src/com/android/server/telecom/CallsManager.java +++ b/src/com/android/server/telecom/CallsManager.java @@ -129,9 +129,11 @@ import com.android.server.telecom.callfiltering.CallScreeningServiceFilter; import com.android.server.telecom.callfiltering.DirectToVoicemailFilter; import com.android.server.telecom.callfiltering.DndCallFilter; import com.android.server.telecom.callfiltering.IncomingCallFilterGraph; +import com.android.server.telecom.callfiltering.IncomingCallFilterGraphProvider; import com.android.server.telecom.callredirection.CallRedirectionProcessor; import com.android.server.telecom.components.ErrorDialogActivity; import com.android.server.telecom.components.TelecomBroadcastReceiver; +import com.android.server.telecom.flags.FeatureFlags; import com.android.server.telecom.stats.CallFailureCause; import com.android.server.telecom.ui.AudioProcessingNotification; import com.android.server.telecom.ui.CallRedirectionTimeoutDialogActivity; @@ -286,15 +288,14 @@ public class CallsManager extends Call.ListenerBase public static final String EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_EMERGENCY_ERROR_MSG = "Exception thrown while retrieving list of potential phone accounts when placing an " + "emergency call."; - public static final UUID EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_UUID = - UUID.fromString("f9a916c8-8d61-4550-9ad3-11c2e84f6364"); - public static final String EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_MSG = - "An emergency call was disconnected after the connection was created but before the " - + "call was successfully added to CallsManager."; public static final UUID EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_UUID = UUID.fromString("2e994acb-1997-4345-8bf3-bad04303de26"); public static final String EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_MSG = "An emergency call was aborted since there were no available phone accounts."; + public static final UUID TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_UUID = + UUID.fromString("0a86157c-50ca-11ee-be56-0242ac120002"); + public static final String TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG = + "Telephony has a default MO acct but Telecom prompted user for MO"; private static final int[] OUTGOING_CALL_STATES = {CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING, @@ -463,6 +464,9 @@ public class CallsManager extends Call.ListenerBase private final TransactionManager mTransactionManager; private final UserManager mUserManager; private final CallStreamingNotification mCallStreamingNotification; + private final FeatureFlags mFeatureFlags; + + private final IncomingCallFilterGraphProvider mIncomingCallFilterGraphProvider; private final ConnectionServiceFocusManager.CallsManagerRequester mRequester = new ConnectionServiceFocusManager.CallsManagerRequester() { @@ -577,7 +581,9 @@ public class CallsManager extends Call.ListenerBase TransactionManager transactionManager, EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger, CallAudioCommunicationDeviceTracker communicationDeviceTracker, - CallStreamingNotification callStreamingNotification) { + CallStreamingNotification callStreamingNotification, + FeatureFlags featureFlags, + IncomingCallFilterGraphProvider incomingCallFilterGraphProvider) { mContext = context; mLock = lock; @@ -596,26 +602,32 @@ public class CallsManager extends Call.ListenerBase mEmergencyCallHelper = emergencyCallHelper; mCallerInfoLookupHelper = callerInfoLookupHelper; mEmergencyCallDiagnosticLogger = emergencyCallDiagnosticLogger; + mIncomingCallFilterGraphProvider = incomingCallFilterGraphProvider; mDtmfLocalTonePlayer = new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy()); - CallAudioRouteStateMachine callAudioRouteStateMachine = - callAudioRouteStateMachineFactory.create( - context, - this, - bluetoothManager, - wiredHeadsetManager, - statusBarNotifier, - audioServiceFactory, - CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT, - asyncCallAudioTaskExecutor, - communicationDeviceTracker - ); - callAudioRouteStateMachine.initialize(); + CallAudioRouteAdapter callAudioRouteAdapter; + if (!featureFlags.useRefactoredAudioRouteSwitching()) { + callAudioRouteAdapter = callAudioRouteStateMachineFactory.create( + context, + this, + bluetoothManager, + wiredHeadsetManager, + statusBarNotifier, + audioServiceFactory, + CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT, + asyncCallAudioTaskExecutor, + communicationDeviceTracker, + featureFlags + ); + } else { + callAudioRouteAdapter = new CallAudioRouteController(); + } + callAudioRouteAdapter.initialize(); CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter = new CallAudioRoutePeripheralAdapter( - callAudioRouteStateMachine, + callAudioRouteAdapter, bluetoothManager, wiredHeadsetManager, mDockManager, @@ -643,21 +655,23 @@ public class CallsManager extends Call.ListenerBase ringtoneFactory, systemVibrator, new Ringer.VibrationEffectProxy(), mInCallController, mContext.getSystemService(NotificationManager.class), - accessibilityManagerAdapter); + accessibilityManagerAdapter, featureFlags); mCallRecordingTonePlayer = new CallRecordingTonePlayer(mContext, audioManager, mTimeoutsAdapter, mLock); - mCallAudioManager = new CallAudioManager(callAudioRouteStateMachine, + mCallAudioManager = new CallAudioManager(callAudioRouteAdapter, this, callAudioModeStateMachineFactory.create(systemStateHelper, - (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)), + (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE), + featureFlags, communicationDeviceTracker), playerFactory, mRinger, new RingbackPlayer(playerFactory), - bluetoothStateReceiver, mDtmfLocalTonePlayer); + bluetoothStateReceiver, mDtmfLocalTonePlayer, featureFlags); mConnectionSvrFocusMgr = connectionServiceFocusManagerFactory.create(mRequester); mHeadsetMediaButton = headsetMediaButtonFactory.create(context, this, mLock); mTtyManager = new TtyManager(context, mWiredHeadsetManager); mProximitySensorManager = proximitySensorManagerFactory.create(context, this); mPhoneStateBroadcaster = new PhoneStateBroadcaster(this); - mCallLogManager = new CallLogManager(context, phoneAccountRegistrar, mMissedCallNotifier); + mCallLogManager = new CallLogManager(context, phoneAccountRegistrar, mMissedCallNotifier, + mAnomalyReporter, featureFlags); mConnectionServiceRepository = new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, mLock, this); mInCallWakeLockController = inCallWakeLockControllerFactory.create(context, this); @@ -669,11 +683,17 @@ public class CallsManager extends Call.ListenerBase mBlockedNumbersAdapter = blockedNumbersAdapter; mCallStreamingController = new CallStreamingController(mContext, mLock); mCallStreamingNotification = callStreamingNotification; + mFeatureFlags = featureFlags; + if (mFeatureFlags.useImprovedListenerOrder()) { + mListeners.add(mInCallController); + } mListeners.add(mInCallWakeLockController); mListeners.add(statusBarNotifier); mListeners.add(mCallLogManager); - mListeners.add(mInCallController); + if (!mFeatureFlags.useImprovedListenerOrder()) { + mListeners.add(mInCallController); + } mListeners.add(mCallEndpointController); mListeners.add(mCallDiagnosticServiceController); mListeners.add(mCallAudioManager); @@ -748,11 +768,14 @@ public class CallsManager extends Call.ListenerBase @Override @VisibleForTesting public void onSuccessfulOutgoingCall(Call call, int callState) { - Log.v(this, "onSuccessfulOutgoingCall, %s", call); + Log.v(this, "onSuccessfulOutgoingCall, call=[%s], state=[%d]", call, callState); call.setPostCallPackageName(getRoleManagerAdapter().getDefaultCallScreeningApp( call.getAssociatedUser())); - setCallState(call, callState, "successful outgoing call"); + if (!mFeatureFlags.fixAudioFlickerForOutgoingCalls()) { + setCallState(call, callState, "successful outgoing call"); + } + if (!mCalls.contains(call)) { // Call was not added previously in startOutgoingCall due to it being a potential MMI // code, so add it now. @@ -764,7 +787,18 @@ public class CallsManager extends Call.ListenerBase listener.onConnectionServiceChanged(call, null, call.getConnectionService()); } - markCallAsDialing(call); + if (mFeatureFlags.fixAudioFlickerForOutgoingCalls()) { + // Allow the ConnectionService to start the call in the active state. This case is + // helpful for conference calls or meetings that can skip the dialing stage. + if (callState == CallState.ACTIVE) { + setCallState(call, callState, "skipping the dialing state and setting active"); + } else { + markCallAsDialing(call); + } + } + else{ + markCallAsDialing(call); + } } @Override @@ -783,11 +817,12 @@ public class CallsManager extends Call.ListenerBase ? new Bundle() : phoneAccount.getExtras(); TelephonyManager telephonyManager = getTelephonyManager(); + boolean performDndFilter = mFeatureFlags.skipFilterPhoneAccountPerformDndFilter(); if (incomingCall.hasProperty(Connection.PROPERTY_EMERGENCY_CALLBACK_MODE) || incomingCall.hasProperty(Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL) || telephonyManager.isInEmergencySmsMode() || incomingCall.isSelfManaged() || - extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING)) { + (!performDndFilter && extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING))) { Log.i(this, "Skipping call filtering for %s (ecm=%b, " + "networkIdentifiedEmergencyCall = %b, emergencySmsMode = %b, " + "selfMgd=%b, skipExtra=%b)", @@ -805,12 +840,27 @@ public class CallsManager extends Call.ListenerBase .build(), false); incomingCall.setIsUsingCallFiltering(false); return; + } else if (performDndFilter && extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING)) { + IncomingCallFilterGraph graph = setupDndFilterOnlyGraph(incomingCall); + graph.performFiltering(); + return; } IncomingCallFilterGraph graph = setUpCallFilterGraph(incomingCall); graph.performFiltering(); } + private IncomingCallFilterGraph setupDndFilterOnlyGraph(Call incomingHfpCall) { + incomingHfpCall.setIsUsingCallFiltering(true); + DndCallFilter dndCallFilter = new DndCallFilter(incomingHfpCall, mRinger); + IncomingCallFilterGraph graph = mIncomingCallFilterGraphProvider.createGraph( + incomingHfpCall, + this::onCallFilteringComplete, mContext, mTimeoutsAdapter, mLock); + graph.addFilter(dndCallFilter); + mGraphHandlerThreads.add(graph.getHandlerThread()); + return graph; + } + private IncomingCallFilterGraph setUpCallFilterGraph(Call incomingCall) { incomingCall.setIsUsingCallFiltering(true); String carrierPackageName = getCarrierPackageName(); @@ -823,7 +873,7 @@ public class CallsManager extends Call.ListenerBase mContext.getPackageManager(), packageName); ParcelableCallUtils.Converter converter = new ParcelableCallUtils.Converter(); - IncomingCallFilterGraph graph = new IncomingCallFilterGraph(incomingCall, + IncomingCallFilterGraph graph = mIncomingCallFilterGraphProvider.createGraph(incomingCall, this::onCallFilteringComplete, mContext, mTimeoutsAdapter, mLock); DirectToVoicemailFilter voicemailFilter = new DirectToVoicemailFilter(incomingCall, mCallerInfoLookupHelper); @@ -990,7 +1040,7 @@ public class CallsManager extends Call.ListenerBase if (result.shouldShowNotification) { Log.i(this, "onCallScreeningCompleted: blocked call, showing notification."); mMissedCallNotifier.showMissedCallNotification( - new MissedCallNotifier.CallInfo(incomingCall)); + new MissedCallNotifier.CallInfo(incomingCall), /* uri= */ null); } } } @@ -1303,7 +1353,7 @@ public class CallsManager extends Call.ListenerBase return mCallAudioManager; } - InCallController getInCallController() { + public InCallController getInCallController() { return mInCallController; } @@ -1383,8 +1433,11 @@ public class CallsManager extends Call.ListenerBase } @VisibleForTesting - public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){ - mAnomalyReporter = mAnomalyReporterAdapter; + public void setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter){ + mAnomalyReporter = anomalyReporterAdapter; + if (mCallLogManager != null) { + mCallLogManager.setAnomalyReporterAdapter(anomalyReporterAdapter); + } } void processIncomingConference(PhoneAccountHandle phoneAccountHandle, Bundle extras) { @@ -1429,7 +1482,8 @@ public class CallsManager extends Call.ListenerBase false /* forceAttachToExistingConnection */, isConference, /* isConference */ mClockProxy, - mToastFactory); + mToastFactory, + mFeatureFlags); // Ensure new calls related to self-managed calls/connections are set as such. This will // be overridden when the actual connection is returned in startCreateConnection, however // doing this now ensures the logs and any other logic will treat this call as self-managed @@ -1456,7 +1510,10 @@ public class CallsManager extends Call.ListenerBase } } // Incoming address was set via EXTRA_INCOMING_CALL_ADDRESS above. - call.setAssociatedUser(phoneAccountHandle.getUserHandle()); + UserHandle associatedUser = UserUtil.getAssociatedUserForCall( + mFeatureFlags.workProfileAssociatedUser(), + getPhoneAccountRegistrar(), getCurrentUserHandle(), phoneAccountHandle); + call.setAssociatedUser(associatedUser); } if (phoneAccount != null) { @@ -1576,15 +1633,19 @@ public class CallsManager extends Call.ListenerBase // Check if the target phone account is possibly in ECBM. call.setIsInECBM(getEmergencyCallHelper() .isLastOutgoingEmergencyCallPAH(call.getTargetPhoneAccount())); - // If the phone account user profile is paused or the call isn't visible to the secondary/ - // guest user, reject the non-emergency incoming call. When the current user is the admin, - // we need to allow the calls to go through if the work profile isn't paused. We should - // always allow emergency calls and also allow non-emergency calls when ECBM is active for - // the phone account. - if ((mUserManager.isQuietModeEnabled(call.getAssociatedUser()) - || (!mUserManager.isUserAdmin(mCurrentUserHandle.getIdentifier()) - && !isCallVisibleForUser(call, mCurrentUserHandle))) - && !call.isEmergencyCall() && !call.isInECBM()) { + + // Check if call is visible to the current user. + boolean isCallHiddenFromProfile = !isCallVisibleForUser(call, mCurrentUserHandle); + // For admins, we should check if the work profile is paused in order to reject + // the call. + if (mUserManager.isUserAdmin(mCurrentUserHandle.getIdentifier())) { + isCallHiddenFromProfile &= mUserManager.isQuietModeEnabled( + call.getAssociatedUser()); + } + + // We should always allow emergency calls and also allow non-emergency calls when ECBM + // is active for the phone account. + if (isCallHiddenFromProfile && !call.isEmergencyCall() && !call.isInECBM()) { Log.d(TAG, "Rejecting non-emergency call because the owner %s is not running.", phoneAccountHandle.getUserHandle()); call.setMissedReason(USER_MISSED_NOT_RUNNING); @@ -1657,11 +1718,15 @@ public class CallsManager extends Call.ListenerBase true /* forceAttachToExistingConnection */, false, /* isConference */ mClockProxy, - mToastFactory); + mToastFactory, + mFeatureFlags); call.initAnalytics(); // For unknown calls, base the associated user off of the target phone account handle. - call.setAssociatedUser(phoneAccountHandle.getUserHandle()); + UserHandle associatedUser = UserUtil.getAssociatedUserForCall( + mFeatureFlags.workProfileAssociatedUser(), + getPhoneAccountRegistrar(), getCurrentUserHandle(), phoneAccountHandle); + call.setAssociatedUser(associatedUser); setIntentExtrasAndStartTime(call, extras); call.addListener(this); notifyStartCreateConnection(call); @@ -1775,7 +1840,8 @@ public class CallsManager extends Call.ListenerBase false /* forceAttachToExistingConnection */, isConference, /* isConference */ mClockProxy, - mToastFactory); + mToastFactory, + mFeatureFlags); if (extras.containsKey(TelecomManager.TRANSACTION_CALL_ID_KEY)) { call.setIsTransactionalCall(true); @@ -2064,6 +2130,21 @@ public class CallsManager extends Call.ListenerBase return CompletableFuture.completedFuture(Pair.create(callToPlace, accountSuggestions.get(0).getPhoneAccountHandle())); } + + // At this point Telecom is requesting the user to select a phone + // account. However, Telephony is reporting that the user has a default + // outgoing account (which is denoted by a non-negative subId number). + // At some point, Telecom and Telephony are out of sync with the default + // outgoing calling account. + if(mFeatureFlags.telephonyHasDefaultButTelecomDoesNot()) { + if (SubscriptionManager.getDefaultVoiceSubscriptionId() != + SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + mAnomalyReporter.reportAnomaly( + TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_UUID, + TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG); + } + } + // This is the state where the user is expected to select an account callToPlace.setState(CallState.SELECT_PHONE_ACCOUNT, "needs account selection"); @@ -2307,6 +2388,15 @@ public class CallsManager extends Call.ListenerBase PhoneAccountHandle phoneAccountHandle = clientExtras.getParcelable( TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE); + PhoneAccount account = + mPhoneAccountRegistrar.getPhoneAccount(phoneAccountHandle, initiatingUser); + boolean isSelfManaged = account != null && account.isSelfManaged(); + // Enforce outgoing call restriction for conference calls. This is handled via + // UserCallIntentProcessor for normal MO calls. + if (UserUtil.hasOutgoingCallsUserRestriction(mContext, initiatingUser, + null, isSelfManaged, CallsManager.class.getCanonicalName())) { + return; + } CompletableFuture<Call> callFuture = startOutgoingCall(participants, phoneAccountHandle, clientExtras, initiatingUser, null/* originalIntent */, callingPackage, true/* isconference*/); @@ -2883,7 +2973,15 @@ public class CallsManager extends Call.ListenerBase // from the client via a transaction before answering. call.answer(videoState); } else { + if (!mFeatureFlags.genAnomReportOnFocusTimeout()) { + Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall(); + Log.d(this, "answerCall: Incoming call = %s Ongoing call %s", call, activeCall); + } // Hold or disconnect the active call and request call focus for the incoming call. + Bundle bundle = new Bundle(); + bundle.putLong(TelecomManager.EXTRA_CALL_ANSWERED_TIME_MILLIS, + mClockProxy.currentTimeMillis()); + call.putConnectionServiceExtras(bundle); holdActiveCallForNewCall(call); mConnectionSvrFocusMgr.requestFocus( call, @@ -3716,11 +3814,6 @@ public class CallsManager extends Call.ListenerBase // Notify listeners that the call was disconnected before being added to CallsManager. // Listeners will not receive onAdded or onRemoved callbacks. if (!mCalls.contains(call)) { - if (call.isEmergencyCall()) { - mAnomalyReporter.reportAnomaly( - EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_UUID, - EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_MSG); - } mListeners.forEach(l -> l.onCreateConnectionFailed(call)); } @@ -4155,7 +4248,8 @@ public class CallsManager extends Call.ListenerBase connectTime, connectElapsedTime, mClockProxy, - mToastFactory); + mToastFactory, + mFeatureFlags); // Unlike connections, conferences are not created first and then notified as create // connection complete from the CS. They originate from the CS and are reported directly to @@ -4173,7 +4267,10 @@ public class CallsManager extends Call.ListenerBase call.setStatusHints(parcelableConference.getStatusHints()); call.putConnectionServiceExtras(parcelableConference.getExtras()); // For conference calls, set the associated user from the target phone account user handle. - call.setAssociatedUser(phoneAccount.getUserHandle()); + UserHandle associatedUser = UserUtil.getAssociatedUserForCall( + mFeatureFlags.workProfileAssociatedUser(), getPhoneAccountRegistrar(), + getCurrentUserHandle(), phoneAccount); + call.setAssociatedUser(associatedUser); // In case this Conference was added via a ConnectionManager, keep track of the original // Connection ID as created by the originating ConnectionService. Bundle extras = parcelableConference.getExtras(); @@ -5211,7 +5308,8 @@ public class CallsManager extends Call.ListenerBase connection.getConnectTimeMillis() /* connectTimeMillis */, connection.getConnectElapsedTimeMillis(), /* connectElapsedTimeMillis */ mClockProxy, - mToastFactory); + mToastFactory, + mFeatureFlags); call.initAnalytics(); call.getAnalytics().setCreatedFromExistingConnection(true); @@ -5226,7 +5324,10 @@ public class CallsManager extends Call.ListenerBase connection.getCallerDisplayNamePresentation()); // For existing connections, use the phone account user handle to determine the user // association with the call. - call.setAssociatedUser(connection.getPhoneAccount().getUserHandle()); + UserHandle associatedUser = UserUtil.getAssociatedUserForCall( + mFeatureFlags.workProfileAssociatedUser(), getPhoneAccountRegistrar(), + getCurrentUserHandle(), connection.getPhoneAccount()); + call.setAssociatedUser(associatedUser); call.addListener(this); call.putConnectionServiceExtras(connection.getExtras()); @@ -5451,7 +5552,7 @@ public class CallsManager extends Call.ListenerBase mCallAudioManager.getCallAudioModeStateMachine().getHandler().post(() -> { mainHandlerLatch.countDown(); }); - mCallAudioManager.getCallAudioRouteStateMachine().getHandler().post(() -> { + mCallAudioManager.getCallAudioRouteAdapter().getAdapterHandler().post(() -> { mainHandlerLatch.countDown(); }); @@ -5477,9 +5578,10 @@ public class CallsManager extends Call.ListenerBase // We are going to place the new outgoing call, so disconnect any ongoing self-managed // calls which are ongoing at this time. disconnectSelfManagedCalls("outgoing call " + callId); - - mPendingCallConfirm.complete(mPendingCall); - mPendingCallConfirm = null; + if (mPendingCallConfirm != null) { + mPendingCallConfirm.complete(mPendingCall); + mPendingCallConfirm = null; + } mPendingCall = null; } } @@ -5498,8 +5600,10 @@ public class CallsManager extends Call.ListenerBase markCallAsDisconnected(mPendingCall, new DisconnectCause(DisconnectCause.CANCELED)); markCallAsRemoved(mPendingCall); mPendingCall = null; - mPendingCallConfirm.complete(null); - mPendingCallConfirm = null; + if (mPendingCallConfirm != null) { + mPendingCallConfirm.complete(null); + mPendingCallConfirm = null; + } } } @@ -5868,7 +5972,7 @@ public class CallsManager extends Call.ListenerBase handoverFromCall.getHandle(), null, null, null, Call.CALL_DIRECTION_OUTGOING, false, - false, mClockProxy, mToastFactory); + false, mClockProxy, mToastFactory, mFeatureFlags); call.initAnalytics(); // Set self-managed and voipAudioMode if destination is self-managed CS @@ -6075,7 +6179,8 @@ public class CallsManager extends Call.ListenerBase false /* forceAttachToExistingConnection */, false, /* isConference */ mClockProxy, - mToastFactory); + mToastFactory, + mFeatureFlags); if (fromCall == null || isHandoverInProgress() || !isHandoverFromPhoneAccountSupported(fromCall.getTargetPhoneAccount()) || diff --git a/src/com/android/server/telecom/ConnectionServiceFocusManager.java b/src/com/android/server/telecom/ConnectionServiceFocusManager.java index 72cb7c423..35be0f898 100644 --- a/src/com/android/server/telecom/ConnectionServiceFocusManager.java +++ b/src/com/android/server/telecom/ConnectionServiceFocusManager.java @@ -30,6 +30,7 @@ import android.util.LogPrinter; import android.util.Printer; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.telecom.flags.Flags; import com.android.internal.util.IndentingPrintWriter; import java.util.ArrayList; @@ -340,18 +341,23 @@ public class ConnectionServiceFocusManager { if (syncCallFocus != null) { return syncCallFocus.orElse(null); } else { - Log.w(TAG, "Timed out waiting for synchronous current focus. Returning possibly" - + " inaccurate result. returning currentFocusCall=[%s]", mCurrentFocusCall); - - // dump the state of the handler to better understand the timeout - mEventHandler.dump( - new LogPrinter(android.util.Log.INFO, TAG), "CsFocusMgr_timeout"); - - // report the timeout - mAnomalyReporter.reportAnomaly( - WATCHDOG_GET_CALL_FOCUS_TIMEOUT_UUID, - WATCHDOG_GET_CALL_FOCUS_TIMEOUT_MSG); - + if (Flags.genAnomReportOnFocusTimeout()) { + Log.w(TAG, "Timed out waiting for synchronous current focus. Returning possibly" + + " inaccurate result. returning currentFocusCall=[%s]", + mCurrentFocusCall); + + // dump the state of the handler to better understand the timeout + mEventHandler.dump( + new LogPrinter(android.util.Log.INFO, TAG), "CsFocusMgr_timeout"); + + // report the timeout + mAnomalyReporter.reportAnomaly( + WATCHDOG_GET_CALL_FOCUS_TIMEOUT_UUID, + WATCHDOG_GET_CALL_FOCUS_TIMEOUT_MSG); + } else { + Log.w(TAG, "Timed out waiting for synchronous current focus. Returning possibly" + + " inaccurate result"); + } return mCurrentFocusCall; } } catch (InterruptedException e) { diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java index de1ececf6..07b048db5 100644 --- a/src/com/android/server/telecom/ConnectionServiceWrapper.java +++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java @@ -33,7 +33,6 @@ import android.os.Bundle; import android.os.CancellationSignal; import android.os.IBinder; import android.os.ParcelFileDescriptor; -import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.UserHandle; @@ -65,19 +64,22 @@ import com.android.internal.telecom.IConnectionServiceAdapter; import com.android.internal.telecom.IVideoProvider; import com.android.internal.telecom.RemoteServiceCallback; import com.android.internal.util.Preconditions; +import com.android.server.telecom.flags.Flags; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.Objects; /** * Wrapper for {@link IConnectionService}s, handles binding to {@link IConnectionService} and keeps @@ -90,9 +92,12 @@ public class ConnectionServiceWrapper extends ServiceBinder implements ConnectionServiceFocusManager.ConnectionServiceFocus { private static final String TELECOM_ABBREVIATION = "cast"; + private static final long SERVICE_BINDING_TIMEOUT = 15000L; private CompletableFuture<Pair<Integer, Location>> mQueryLocationFuture = null; private @Nullable CancellationSignal mOngoingQueryLocationRequest = null; private final ExecutorService mQueryLocationExecutor = Executors.newSingleThreadExecutor(); + private ScheduledExecutorService mScheduledExecutor = + Executors.newSingleThreadScheduledExecutor(); private final class Adapter extends IConnectionServiceAdapter.Stub { @@ -520,8 +525,6 @@ public class ConnectionServiceWrapper extends ServiceBinder implements .validateAccountIconUserBoundary(icon, callingUserHandle)); } - if (ConnectionServiceWrapper.this.mIsRemoteConnectionService) return; - if (parcelableConference.getConnectElapsedTimeMillis() != 0 && mContext.checkCallingOrSelfPermission(MODIFY_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) { @@ -936,9 +939,6 @@ public class ConnectionServiceWrapper extends ServiceBinder implements public void addExistingConnection(String callId, ParcelableConnection connection, Session.Info sessionInfo) { Log.startSession(sessionInfo, "CSW.aEC", mPackageAbbreviation); - - if (ConnectionServiceWrapper.this.mIsRemoteConnectionService) return; - UserHandle userHandle = Binder.getCallingUserHandle(); // Check that the Calling Package matches PhoneAccountHandle's Component Package PhoneAccountHandle callingPhoneAccountHandle = connection.getPhoneAccount(); @@ -1353,7 +1353,6 @@ public class ConnectionServiceWrapper extends ServiceBinder implements private final CallsManager mCallsManager; private final AppOpsManager mAppOpsManager; private final Context mContext; - public boolean mIsRemoteConnectionService = false; private ConnectionServiceFocusManager.ConnectionServiceFocusListener mConnSvrFocusListener; @@ -1599,7 +1598,22 @@ public class ConnectionServiceWrapper extends ServiceBinder implements .setParticipants(call.getParticipants()) .setIsAdhocConferenceCall(call.isAdhocConferenceCall()) .build(); - + if (Flags.unbindTimeoutConnections()) { + android.telecom.Logging.Runnable r = + new android.telecom.Logging.Runnable("CSW.cC", mLock) { + @Override + public void loggedRun() { + if (!call.isCreateConnectionComplete()) { + Log.e(this, new Exception(), "Conference %s creation timeout", + getComponentName()); + response.handleCreateConferenceFailure( + new DisconnectCause(DisconnectCause.ERROR)); + } + } + }; + mScheduledExecutor.schedule(r.getRunnableToCancel(), SERVICE_BINDING_TIMEOUT, + TimeUnit.MILLISECONDS); + } try { mServiceInterface.createConference( call.getConnectionManagerPhoneAccount(), @@ -1608,7 +1622,6 @@ public class ConnectionServiceWrapper extends ServiceBinder implements call.shouldAttachToExistingConnection(), call.isUnknown(), Log.getExternalSession(TELECOM_ABBREVIATION)); - } catch (RemoteException e) { Log.e(this, e, "Failure to createConference -- %s", getComponentName()); mPendingResponses.remove(callId).handleCreateConferenceFailure( @@ -1641,6 +1654,7 @@ public class ConnectionServiceWrapper extends ServiceBinder implements Log.i(ConnectionServiceWrapper.this, "Call not present" + " in call id mapper, maybe it was aborted before the bind" + " completed successfully?"); + response.handleCreateConnectionFailure( new DisconnectCause(DisconnectCause.CANCELED)); return; @@ -1702,6 +1716,23 @@ public class ConnectionServiceWrapper extends ServiceBinder implements .setRttPipeToInCall(call.getCsToInCallRttPipeForCs()) .build(); + if (Flags.unbindTimeoutConnections()) { + android.telecom.Logging.Runnable r = + new android.telecom.Logging.Runnable("CSW.cC", mLock) { + @Override + public void loggedRun() { + if (!call.isCreateConnectionComplete()) { + Log.e(this, new Exception(), + "Connection %s creation timeout", + getComponentName()); + response.handleCreateConnectionFailure( + new DisconnectCause(DisconnectCause.ERROR)); + } + } + }; + mScheduledExecutor.schedule(r.getRunnableToCancel(), SERVICE_BINDING_TIMEOUT, + TimeUnit.MILLISECONDS); + } try { mServiceInterface.createConnection( call.getConnectionManagerPhoneAccount(), @@ -1710,7 +1741,6 @@ public class ConnectionServiceWrapper extends ServiceBinder implements call.shouldAttachToExistingConnection(), call.isUnknown(), Log.getExternalSession(TELECOM_ABBREVIATION)); - } catch (RemoteException e) { Log.e(this, e, "Failure to createConnection -- %s", getComponentName()); mPendingResponses.remove(callId).handleCreateConnectionFailure( @@ -2159,7 +2189,8 @@ public class ConnectionServiceWrapper extends ServiceBinder implements } } - void addCall(Call call) { + @VisibleForTesting + public void addCall(Call call) { if (mCallIdMapper.getCallId(call) == null) { mCallIdMapper.addCall(call); } @@ -2395,6 +2426,7 @@ public class ConnectionServiceWrapper extends ServiceBinder implements BindCallback callback = new BindCallback() { @Override public void onSuccess() { + if (!isServiceValid("connectionServiceFocusLost")) return; try { mServiceInterface.connectionServiceFocusLost( Log.getExternalSession(TELECOM_ABBREVIATION)); @@ -2414,6 +2446,7 @@ public class ConnectionServiceWrapper extends ServiceBinder implements BindCallback callback = new BindCallback() { @Override public void onSuccess() { + if (!isServiceValid("connectionServiceFocusGained")) return; try { mServiceInterface.connectionServiceFocusGained( Log.getExternalSession(TELECOM_ABBREVIATION)); @@ -2492,12 +2525,11 @@ public class ConnectionServiceWrapper extends ServiceBinder implements */ private void handleConnectionServiceDeath() { if (!mPendingResponses.isEmpty()) { - CreateConnectionResponse[] responses = mPendingResponses.values().toArray( - new CreateConnectionResponse[mPendingResponses.values().size()]); + Collection<CreateConnectionResponse> responses = mPendingResponses.values(); mPendingResponses.clear(); - for (int i = 0; i < responses.length; i++) { - responses[i].handleCreateConnectionFailure( - new DisconnectCause(DisconnectCause.ERROR, "CS_DEATH")); + for (CreateConnectionResponse response : responses) { + response.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.ERROR, + "CS_DEATH")); } } mCallIdMapper.clear(); @@ -2510,13 +2542,13 @@ public class ConnectionServiceWrapper extends ServiceBinder implements private void logIncoming(String msg, Object... params) { // Keep these as debug; the incoming logging is traced on a package level through the // session logging. - Log.d(this, "CS -> TC[" + Log.getPackageAbbreviation(mComponentName) + "]:" - + " isRCS = " + this.mIsRemoteConnectionService + ": " + msg, params); + Log.d(this, "CS -> TC[" + Log.getPackageAbbreviation(mComponentName) + "]: " + + msg, params); } private void logOutgoing(String msg, Object... params) { - Log.d(this, "TC -> CS[" + Log.getPackageAbbreviation(mComponentName) + "]:" - + " isRCS = " + this.mIsRemoteConnectionService + ": " + msg, params); + Log.d(this, "TC -> CS[" + Log.getPackageAbbreviation(mComponentName) + "]: " + + msg, params); } private void queryRemoteConnectionServices(final UserHandle userHandle, @@ -2543,7 +2575,6 @@ public class ConnectionServiceWrapper extends ServiceBinder implements ConnectionServiceWrapper service = mConnectionServiceRepository.getService( handle.getComponentName(), handle.getUserHandle()); if (service != null && service != this) { - service.mIsRemoteConnectionService = true; simServices.add(service); } else { // This is unexpected, normally PhoneAccounts with CAPABILITY_CALL_PROVIDER are not @@ -2627,4 +2658,9 @@ public class ConnectionServiceWrapper extends ServiceBinder implements sb.append("]"); return sb.toString(); } + + @VisibleForTesting + public void setScheduledExecutorService(ScheduledExecutorService service) { + mScheduledExecutor = service; + } } diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java index d5689aef7..1aee25c75 100644 --- a/src/com/android/server/telecom/InCallController.java +++ b/src/com/android/server/telecom/InCallController.java @@ -22,7 +22,9 @@ import static android.os.Process.myUid; import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManager; import android.app.AppOpsManager; +import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationManager; import android.content.AttributionSource; @@ -47,7 +49,6 @@ import android.os.RemoteException; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; -import android.permission.PermissionManager; import android.telecom.CallAudioState; import android.telecom.CallEndpoint; import android.telecom.ConnectionService; @@ -66,6 +67,7 @@ import com.android.internal.telecom.IInCallService; import com.android.internal.util.ArrayUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.server.telecom.SystemStateHelper.SystemStateListener; +import com.android.server.telecom.flags.FeatureFlags; import com.android.server.telecom.ui.NotificationChannelManager; import java.util.ArrayList; @@ -80,6 +82,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it @@ -1237,11 +1240,12 @@ public class InCallController extends CallsManagerListenerBase implements private ArraySet<String> mAllCarrierPrivilegedApps = new ArraySet<>(); private ArraySet<String> mActiveCarrierPrivilegedApps = new ArraySet<>(); + private FeatureFlags mFeatureFlags; public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager, SystemStateHelper systemStateHelper, DefaultDialerCache defaultDialerCache, Timeouts.Adapter timeoutsAdapter, EmergencyCallHelper emergencyCallHelper, - CarModeTracker carModeTracker, ClockProxy clockProxy) { + CarModeTracker carModeTracker, ClockProxy clockProxy, FeatureFlags featureFlags) { mContext = context; mAppOpsManager = context.getSystemService(AppOpsManager.class); mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class); @@ -1258,6 +1262,7 @@ public class InCallController extends CallsManagerListenerBase implements IntentFilter userAddedFilter = new IntentFilter(Intent.ACTION_USER_ADDED); userAddedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); mContext.registerReceiver(mUserAddedReceiver, userAddedFilter); + mFeatureFlags = featureFlags; } private void restrictPhoneCallOps() { @@ -1402,17 +1407,31 @@ public class InCallController extends CallsManagerListenerBase implements @Override public void onCallRemoved(Call call) { Log.i(this, "onCallRemoved: %s", call); - if (mCallsManager.getCalls().isEmpty()) { + // Instead of checking if there are no active calls, we should check if there any calls with + // the same associated user returned from getUserFromCall. For instance, it's possible to + // have calls coexist on the personal profile and work profile, in which case, we would only + // remove the ICS connection for the user associated with the call to be disconnected. + UserHandle userFromCall = getUserFromCall(call); + Stream<Call> callsAssociatedWithUserFromCall = mCallsManager.getCalls().stream() + .filter((c) -> getUserFromCall(c).equals(userFromCall)); + boolean isCallCountZero = mFeatureFlags.workProfileAssociatedUser() + ? callsAssociatedWithUserFromCall.count() == 0 + : mCallsManager.getCalls().isEmpty(); + if (isCallCountZero) { /** Let's add a 2 second delay before we send unbind to the services to hopefully * give them enough time to process all the pending messages. */ mHandler.postDelayed(new Runnable("ICC.oCR", mLock) { @Override public void loggedRun() { - // Check again to make sure there are no active calls. - if (mCallsManager.getCalls().isEmpty()) { - unbindFromServices(getUserFromCall(call)); - + // Check again to make sure there are no active calls for the associated user. + Stream<Call> callsAssociatedWithUserFromCall = mCallsManager.getCalls().stream() + .filter((c) -> getUserFromCall(c).equals(userFromCall)); + boolean isCallCountZero = mFeatureFlags.workProfileAssociatedUser() + ? callsAssociatedWithUserFromCall.count() == 0 + : mCallsManager.getCalls().isEmpty(); + if (isCallCountZero) { + unbindFromServices(userFromCall); mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission(); } } @@ -1690,6 +1709,29 @@ public class InCallController extends CallsManagerListenerBase implements @VisibleForTesting public void bringToForeground(boolean showDialpad, UserHandle callingUser) { + KeyguardManager keyguardManager = mContext.getSystemService(KeyguardManager.class); + boolean isLockscreenRestricted = keyguardManager != null + && keyguardManager.isKeyguardLocked(); + UserHandle currentUser = mCallsManager.getCurrentUserHandle(); + // Handle cases when calls are placed from the keyguard UI screen, which operates under + // the admin user. This needs to account for emergency calls placed from secondary/guest + // users as well as the work profile. Once the screen is locked, the user should be able to + // return to the call (from the keyguard UI). + if (mFeatureFlags.eccKeyguard() && mCallsManager.isInEmergencyCall() + && isLockscreenRestricted && !mInCallServices.containsKey(callingUser)) { + // If screen is locked and the current user is the system, query calls for the work + // profile user, if available. Otherwise, the user is in the secondary/guest profile, + // so we can default to the system user. + if (currentUser.isSystem()) { + UserManager um = mContext.getSystemService(UserManager.class); + UserHandle workProfileUser = findChildManagedProfileUser(currentUser, um); + boolean hasWorkCalls = mCallsManager.getCalls().stream() + .filter((c) -> getUserFromCall(c).equals(workProfileUser)).count() > 0; + callingUser = hasWorkCalls ? workProfileUser : currentUser; + } else { + callingUser = currentUser; + } + } if (mInCallServices.containsKey(callingUser)) { for (IInCallService inCallService : mInCallServices.get(callingUser).values()) { try { @@ -1805,6 +1847,7 @@ public class InCallController extends CallsManagerListenerBase implements * Unbinds an existing bound connection to the in-call app. */ public void unbindFromServices(UserHandle userHandle) { + Log.i(this, "Unbinding from services for user %s", userHandle); try { mContext.unregisterReceiver(mPackageChangedReceiver); } catch (IllegalArgumentException e) { @@ -2121,7 +2164,7 @@ public class InCallController extends CallsManagerListenerBase implements ComponentName foundComponentName = new ComponentName(serviceInfo.packageName, serviceInfo.name); - if (requestedType == IN_CALL_SERVICE_TYPE_NON_UI) { + if (currentType == IN_CALL_SERVICE_TYPE_NON_UI) { mKnownNonUiInCallServices.add(foundComponentName); } @@ -2293,7 +2336,9 @@ public class InCallController extends CallsManagerListenerBase implements } // Upon successful connection, send the state of the world to the service. - List<Call> calls = orderCallsWithChildrenFirst(mCallsManager.getCalls()); + List<Call> calls = orderCallsWithChildrenFirst(mCallsManager.getCalls().stream().filter( + call -> getUserFromCall(call).equals(userHandle)) + .collect(Collectors.toUnmodifiableList())); Log.i(this, "Adding %s calls to InCallService after onConnected: %s, including external " + "calls", calls.size(), info.getComponentName()); int numCallsSent = 0; @@ -2430,10 +2475,15 @@ public class InCallController extends CallsManagerListenerBase implements try { inCallService.updateCall( sanitizeParcelableCallForService(info, parcelableCall)); - } catch (RemoteException ignored) { + } catch (RemoteException exception) { + Log.w(this, "Call status update did not send to: " + + componentName +" successfully with error " + exception); } } Log.i(this, "Components updated: %s", componentsUpdated); + } else { + Log.i(this, + "Unable to update call. InCallService not found for user: %s", userFromCall); } } @@ -2872,8 +2922,11 @@ public class InCallController extends CallsManagerListenerBase implements } else { UserHandle userFromCall = call.getAssociatedUser(); UserManager userManager = mContext.getSystemService(UserManager.class); - // Emergency call should never be blocked, so if the user associated with call is in - // quite mode, use the primary user for the emergency call. + // Emergency call should never be blocked, so if the user associated with the target + // phone account handle user is in quiet mode, use the current user for the ecall. + // Note, that this only applies to incoming calls that are received on assigned + // sims (i.e. work sim), where the associated user would be the target phone account + // handle user. if ((call.isEmergencyCall() || call.isInECBM()) && (userManager.isQuietModeEnabled(userFromCall) // We should also account for secondary/guest users where the profile may not @@ -2885,4 +2938,25 @@ public class InCallController extends CallsManagerListenerBase implements return userFromCall; } } + + /** + * Useful for debugging purposes and called on the command line via + * an "adb shell telecom command". + * + * @return true if a particular non-ui InCallService package is bound in a call. + */ + public boolean isNonUiInCallServiceBound(String packageName) { + for (NonUIInCallServiceConnectionCollection ics : mNonUIInCallServiceConnections.values()) { + for (InCallServiceBindingConnection connection : ics.getSubConnections()) { + InCallServiceInfo serviceInfo = connection.mInCallServiceInfo; + Log.i(this, "isNonUiInCallServiceBound: found serviceInfo=[%s]", serviceInfo); + if (serviceInfo != null && + serviceInfo.mComponentName.getPackageName().contains(packageName)) { + Log.i(this, "isNonUiInCallServiceBound: found target package"); + return true; + } + } + } + return false; + } } diff --git a/src/com/android/server/telecom/MissedCallNotifier.java b/src/com/android/server/telecom/MissedCallNotifier.java index 0e5a2874d..b0a7c8ecb 100644 --- a/src/com/android/server/telecom/MissedCallNotifier.java +++ b/src/com/android/server/telecom/MissedCallNotifier.java @@ -16,6 +16,7 @@ package com.android.server.telecom; +import android.annotation.Nullable; import android.net.Uri; import android.os.UserHandle; import android.telecom.PhoneAccountHandle; @@ -85,7 +86,7 @@ public interface MissedCallNotifier extends CallsManager.CallsManagerListener { void clearMissedCalls(UserHandle userHandle); - void showMissedCallNotification(CallInfo call); + void showMissedCallNotification(CallInfo call, @Nullable Uri uri); void reloadAfterBootComplete(CallerInfoLookupHelper callerInfoLookupHelper, CallInfoFactory callInfoFactory); diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java index 3b402b1aa..6070baa5f 100644 --- a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java +++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java @@ -16,6 +16,7 @@ package com.android.server.telecom; +import android.Manifest; import android.app.Activity; import android.app.AppOpsManager; import android.app.BroadcastOptions; @@ -37,6 +38,7 @@ import android.telephony.TelephonyManager; import android.text.TextUtils; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.telecom.flags.FeatureFlags; import com.android.server.telecom.callredirection.CallRedirectionProcessor; // TODO: Needed for move to system service: import com.android.internal.R; @@ -77,6 +79,7 @@ public class NewOutgoingCallIntentBroadcaster { private final TelecomSystem.SyncRoot mLock; private final DefaultDialerCache mDefaultDialerCache; private final MmiUtils mMmiUtils; + private final FeatureFlags mFeatureFlags; /* * Whether or not the outgoing call intent originated from the default phone application. If @@ -100,7 +103,8 @@ public class NewOutgoingCallIntentBroadcaster { @VisibleForTesting public NewOutgoingCallIntentBroadcaster(Context context, CallsManager callsManager, Intent intent, PhoneNumberUtilsAdapter phoneNumberUtilsAdapter, - boolean isDefaultPhoneApp, DefaultDialerCache defaultDialerCache, MmiUtils mmiUtils) { + boolean isDefaultPhoneApp, DefaultDialerCache defaultDialerCache, MmiUtils mmiUtils, + FeatureFlags featureFlags) { mContext = context; mCallsManager = callsManager; mIntent = intent; @@ -109,6 +113,7 @@ public class NewOutgoingCallIntentBroadcaster { mLock = mCallsManager.getLock(); mDefaultDialerCache = defaultDialerCache; mMmiUtils = mmiUtils; + mFeatureFlags = featureFlags; } /** @@ -128,7 +133,8 @@ public class NewOutgoingCallIntentBroadcaster { // Once the NEW_OUTGOING_CALL broadcast is finished, the resultData is // used as the actual number to call. (If null, no call will be placed.) String resultNumber = getResultData(); - Log.i(this, "Received new-outgoing-call-broadcast for %s with data %s", mCall, + Log.i(NewOutgoingCallIntentBroadcaster.this, + "Received new-outgoing-call-broadcast for %s with data %s", mCall, Log.pii(resultNumber)); boolean endEarly = false; @@ -320,6 +326,7 @@ public class NewOutgoingCallIntentBroadcaster { String scheme = mPhoneNumberUtilsAdapter.isUriNumber(number) ? PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL; result.callingAddress = Uri.fromParts(scheme, number, null); + return result; } @@ -351,14 +358,57 @@ public class NewOutgoingCallIntentBroadcaster { public void processCall(Call call, CallDisposition disposition) { mCall = call; + + // If the new outgoing call broadast doesn't block, trigger the legacy process call + // behavior and exit out here. + if (!mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()) { + legacyProcessCall(disposition); + return; + } + boolean callRedirectionWithService = false; + // Only try to do redirection if it was requested and we're not calling immediately. + // We can expect callImmediately to be true for emergency calls and voip calls. + if (disposition.requestRedirection && !disposition.callImmediately) { + CallRedirectionProcessor callRedirectionProcessor = new CallRedirectionProcessor( + mContext, mCallsManager, mCall, disposition.callingAddress, + mCallsManager.getPhoneAccountRegistrar(), + getGateWayInfoFromIntent(mIntent, mIntent.getData()), + mIntent.getBooleanExtra(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, + false), + mIntent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, + VideoProfile.STATE_AUDIO_ONLY)); + /** + * If there is an available {@link android.telecom.CallRedirectionService}, use the + * {@link CallRedirectionProcessor} to perform call redirection instead of using + * broadcasting. + */ + callRedirectionWithService = callRedirectionProcessor + .canMakeCallRedirectionWithServiceAsUser(mCall.getAssociatedUser()); + if (callRedirectionWithService) { + callRedirectionProcessor.performCallRedirection(mCall.getAssociatedUser()); + } + } + + // If no redirection was kicked off, place the call now. + if (!callRedirectionWithService) { + callImmediately(disposition); + } + + // Finally, send the non-blocking broadcast if we're supposed to (ie for any non-voip call). + if (disposition.sendBroadcast) { + UserHandle targetUser = mCall.getAssociatedUser(); + broadcastIntent(mIntent, disposition.number, false /* receiverRequired */, targetUser); + } + } + + /** + * The legacy non-flagged version of processing a call. Although there is some code duplication + * if makes the new flow cleaner to read. + * @param disposition + */ + private void legacyProcessCall(CallDisposition disposition) { if (disposition.callImmediately) { - boolean speakerphoneOn = mIntent.getBooleanExtra( - TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false); - int videoState = mIntent.getIntExtra( - TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, - VideoProfile.STATE_AUDIO_ONLY); - placeOutgoingCallImmediately(mCall, disposition.callingAddress, null, - speakerphoneOn, videoState); + callImmediately(disposition); // Don't return but instead continue and send the ACTION_NEW_OUTGOING_CALL broadcast // so that third parties can still inspect (but not intercept) the outgoing call. When @@ -390,13 +440,26 @@ public class NewOutgoingCallIntentBroadcaster { if (disposition.sendBroadcast) { UserHandle targetUser = mCall.getAssociatedUser(); - Log.i(this, "Sending NewOutgoingCallBroadcast for %s to %s", mCall, targetUser); broadcastIntent(mIntent, disposition.number, !disposition.callImmediately && !callRedirectionWithService, targetUser); } } /** + * Place a call immediately. + * @param disposition The disposition; used for retrieving the address of the call. + */ + private void callImmediately(CallDisposition disposition) { + boolean speakerphoneOn = mIntent.getBooleanExtra( + TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false); + int videoState = mIntent.getIntExtra( + TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, + VideoProfile.STATE_AUDIO_ONLY); + placeOutgoingCallImmediately(mCall, disposition.callingAddress, null, + speakerphoneOn, videoState); + } + + /** * Sends a new outgoing call ordered broadcast so that third party apps can cancel the * placement of the call or redirect it to a different number. * @@ -415,28 +478,51 @@ public class NewOutgoingCallIntentBroadcaster { if (number != null) { broadcastIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number); } - - // Force receivers of this broadcast intent to run at foreground priority because we - // want to finish processing the broadcast intent as soon as possible. - broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND - | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); Log.v(this, "Broadcasting intent: %s.", broadcastIntent); checkAndCopyProviderExtras(originalCallIntent, broadcastIntent); - final BroadcastOptions options = BroadcastOptions.makeBasic(); - options.setBackgroundActivityStartsAllowed(true); - mContext.sendOrderedBroadcastAsUser( - broadcastIntent, - targetUser, - android.Manifest.permission.PROCESS_OUTGOING_CALLS, - AppOpsManager.OP_PROCESS_OUTGOING_CALLS, - options.toBundle(), - receiverRequired ? new NewOutgoingCallBroadcastIntentReceiver() : null, - null, // scheduler - Activity.RESULT_OK, // initialCode - number, // initialData: initial value for the result data (number to be modified) - null); // initialExtras + if (mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()) { + // Where the new outgoing call broadcast is unblocking, do not give receiver FG priority + // and do not allow background activity starts. + broadcastIntent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); + Log.i(this, "broadcastIntent: Sending non-blocking for %s to %s", mCall.getId(), + targetUser); + if (mFeatureFlags.telecomResolveHiddenDependencies()) { + mContext.sendBroadcastAsUser( + broadcastIntent, + targetUser, + Manifest.permission.PROCESS_OUTGOING_CALLS); + } else { + mContext.sendBroadcastAsUser( + broadcastIntent, + targetUser, + android.Manifest.permission.PROCESS_OUTGOING_CALLS, + AppOpsManager.OP_PROCESS_OUTGOING_CALLS); // initialExtras + } + } else { + Log.i(this, "broadcastIntent: Sending ordered for %s to %s, waitForResult=%b", + mCall.getId(), targetUser, receiverRequired); + final BroadcastOptions options = BroadcastOptions.makeBasic(); + options.setBackgroundActivityStartsAllowed(true); + // Force receivers of this broadcast intent to run at foreground priority because we + // want to finish processing the broadcast intent as soon as possible. + broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND + | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); + + mContext.sendOrderedBroadcastAsUser( + broadcastIntent, + targetUser, + android.Manifest.permission.PROCESS_OUTGOING_CALLS, + AppOpsManager.OP_PROCESS_OUTGOING_CALLS, + options.toBundle(), + receiverRequired ? new NewOutgoingCallBroadcastIntentReceiver() : null, + null, // scheduler + Activity.RESULT_OK, // initialCode + number, // initialData: initial value for the result data (number to be + // modified) + null); // initialExtras + } } /** diff --git a/src/com/android/server/telecom/ParcelableCallUtils.java b/src/com/android/server/telecom/ParcelableCallUtils.java index 673b99a9b..c77e6050d 100644 --- a/src/com/android/server/telecom/ParcelableCallUtils.java +++ b/src/com/android/server/telecom/ParcelableCallUtils.java @@ -158,6 +158,10 @@ public class ParcelableCallUtils { properties |= android.telecom.Call.Details.PROPERTY_VOIP_AUDIO_MODE; } + if (call.isTransactionalCall()) { + properties |= android.telecom.Call.Details.PROPERTY_IS_TRANSACTIONAL; + } + // If this is a single-SIM device, the "default SIM" will always be the only SIM. boolean isDefaultSmsAccount = phoneAccountRegistrar != null && phoneAccountRegistrar.isUserSelectedSmsPhoneAccount(call.getTargetPhoneAccount()); diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java index acf07e35a..5f23e4d70 100644 --- a/src/com/android/server/telecom/PhoneAccountRegistrar.java +++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java @@ -61,6 +61,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.XmlUtils; import com.android.modules.utils.ModifiedUtf8; +import com.android.server.telecom.flags.Flags; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -389,26 +390,62 @@ public class PhoneAccountRegistrar { account.getGroupId())); } - // Potentially update the default voice subid in SubscriptionManager. + // Potentially update the default voice subid in SubscriptionManager so that Telephony and + // Telecom are in sync. int newSubId = accountHandle == null ? SubscriptionManager.INVALID_SUBSCRIPTION_ID : getSubscriptionIdForPhoneAccount(accountHandle); - if (isSimAccount || accountHandle == null) { - int currentVoiceSubId = mSubscriptionManager.getDefaultVoiceSubscriptionId(); - if (newSubId != currentVoiceSubId) { - Log.i(this, "setUserSelectedOutgoingPhoneAccount: update voice sub; " - + "account=%s, subId=%d", accountHandle, newSubId); - mSubscriptionManager.setDefaultVoiceSubscriptionId(newSubId); + if (Flags.onlyUpdateTelephonyOnValidSubIds()) { + if (shouldUpdateTelephonyDefaultVoiceSubId(accountHandle, isSimAccount, newSubId)) { + updateDefaultVoiceSubId(newSubId, accountHandle); } else { - Log.i(this, "setUserSelectedOutgoingPhoneAccount: no change to voice sub"); + Log.i(this, "setUserSelectedOutgoingPhoneAccount: %s is not a sub", accountHandle); } } else { - Log.i(this, "setUserSelectedOutgoingPhoneAccount: %s is not a sub", accountHandle); + if (isSimAccount || accountHandle == null) { + updateDefaultVoiceSubId(newSubId, accountHandle); + } else { + Log.i(this, "setUserSelectedOutgoingPhoneAccount: %s is not a sub", accountHandle); + } } - write(); fireDefaultOutgoingChanged(); } + private void updateDefaultVoiceSubId(int newSubId, PhoneAccountHandle accountHandle){ + int currentVoiceSubId = mSubscriptionManager.getDefaultVoiceSubscriptionId(); + if (newSubId != currentVoiceSubId) { + Log.i(this, "setUserSelectedOutgoingPhoneAccount: update voice sub; " + + "account=%s, subId=%d", accountHandle, newSubId); + mSubscriptionManager.setDefaultVoiceSubscriptionId(newSubId); + } else { + Log.i(this, "setUserSelectedOutgoingPhoneAccount: no change to voice sub"); + } + } + + // This helper is important for CTS testing. [PhoneAccount]s created by Telecom in CTS are + // assigned a subId value of INVALID_SUBSCRIPTION_ID (-1) by Telephony. However, when + // Telephony has a default outgoing calling voice account of -1, that translates to no default + // account (user should be prompted to select an acct when making MOs). In order to avoid + // Telephony clearing out the newly changed default [PhoneAccount] in Telecom, Telephony should + // not be updated. This situation will never occur in production since [PhoneAccount]s in + // production are assigned non-negative subId values. + private boolean shouldUpdateTelephonyDefaultVoiceSubId(PhoneAccountHandle phoneAccountHandle, + boolean isSimAccount, int newSubId) { + // user requests no call preference + if (phoneAccountHandle == null) { + return true; + } + // do not update Telephony if the newSubId is invalid + if (newSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + Log.w(this, "shouldUpdateTelephonyDefaultVoiceSubId: " + + "invalid subId scenario, not updating Telephony. " + + "phoneAccountHandle=[%s], isSimAccount=[%b], newSubId=[%s]", + phoneAccountHandle, isSimAccount, newSubId); + return false; + } + return isSimAccount; + } + boolean isUserSelectedSmsPhoneAccount(PhoneAccountHandle accountHandle) { return getSubscriptionIdForPhoneAccount(accountHandle) == SubscriptionManager.getDefaultSmsSubscriptionId(); @@ -897,13 +934,15 @@ public class PhoneAccountRegistrar { * @throws IllegalArgumentException if MAX_PHONE_ACCOUNT_REGISTRATIONS are reached */ private void enforceMaxPhoneAccountLimit(@NonNull PhoneAccount account) { - final PhoneAccountHandle accountHandle = account.getAccountHandle(); - final UserHandle user = accountHandle.getUserHandle(); - final ComponentName componentName = accountHandle.getComponentName(); - - if (getPhoneAccountHandles(0, null, componentName.getPackageName(), - true /* includeDisabled */, user, false /* crossUserAccess */).size() - >= MAX_PHONE_ACCOUNT_REGISTRATIONS) { + List<PhoneAccount> unverifiedAccounts = getAccountsForPackage_BypassResolveComp( + account.getAccountHandle().getComponentName().getPackageName(), + account.getAccountHandle().getUserHandle()); + // verify each phone account is backed by a valid ConnectionService. If the + // ConnectionService has been disabled or cannot be resolved, unregister the accounts. + List<PhoneAccount> verifiedAccounts = + cleanupUnresolvableConnectionServiceAccounts(unverifiedAccounts); + // enforce the max phone account limit for the application registering accounts + if (verifiedAccounts.size() >= MAX_PHONE_ACCOUNT_REGISTRATIONS) { EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(), "enforceMaxPhoneAccountLimit"); throw new IllegalArgumentException( @@ -1550,6 +1589,51 @@ public class PhoneAccountRegistrar { } /** + * This getter should be used when you want to bypass the {@link + * PhoneAccountRegistrar#resolveComponent(PhoneAccountHandle)} check when fetching accounts + */ + @VisibleForTesting + public List<PhoneAccount> getAccountsForPackage_BypassResolveComp(String packageName, + UserHandle userHandle) { + List<PhoneAccount> accounts = new ArrayList<>(mState.accounts.size()); + for (PhoneAccount m : mState.accounts) { + PhoneAccountHandle handle = m.getAccountHandle(); + + if (packageName != null && !packageName.equals( + handle.getComponentName().getPackageName())) { + // Not the right package name; skip this one. + continue; + } + + if (!isVisibleForUser(m, userHandle, false)) { + // Account is not visible for the current user; skip this one. + continue; + } + accounts.add(m); + } + return accounts; + } + + @VisibleForTesting + public List<PhoneAccount> cleanupUnresolvableConnectionServiceAccounts( + List<PhoneAccount> accounts) { + ArrayList<PhoneAccount> verifiedAccounts = new ArrayList<>(); + for (PhoneAccount account : accounts) { + PhoneAccountHandle handle = account.getAccountHandle(); + // if the ConnectionService has been disabled or can longer be found, remove the handle + if (resolveComponent(handle).isEmpty()) { + Log.i(this, + "Cannot resolve the ConnectionService for handle=[%s]; unregistering" + + " account", handle); + unregisterPhoneAccount(handle); + } else { + verifiedAccounts.add(account); + } + } + return verifiedAccounts; + } + + /** * Clean up the orphan {@code PhoneAccount}. An orphan {@code PhoneAccount} is a phone * account that does not have a {@code UserHandle} or belongs to a deleted package. * @@ -1662,6 +1746,7 @@ public class PhoneAccountRegistrar { } else { pw.println(defaultOutgoing); } + pw.println("defaultVoiceSubId: " + SubscriptionManager.getDefaultVoiceSubscriptionId()); pw.println("simCallManager: " + getSimCallManager(mCurrentUserHandle)); pw.println("phoneAccounts:"); pw.increaseIndent(); diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java index 16dc5c462..5b86b8272 100644 --- a/src/com/android/server/telecom/Ringer.java +++ b/src/com/android/server/telecom/Ringer.java @@ -22,10 +22,12 @@ import static android.provider.CallLog.Calls.USER_MISSED_NO_VIBRATE; import static android.provider.Settings.Global.ZEN_MODE_OFF; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.Notification; import android.app.NotificationManager; import android.app.Person; import android.content.Context; +import android.content.res.Resources; import android.media.AudioManager; import android.media.Ringtone; import android.media.VolumeShaper; @@ -38,13 +40,21 @@ import android.os.UserManager; import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; +import android.os.vibrator.persistence.ParsedVibration; +import android.os.vibrator.persistence.VibrationXmlParser; import android.telecom.Log; import android.telecom.TelecomManager; +import android.text.TextUtils; import android.view.accessibility.AccessibilityManager; import com.android.internal.annotations.VisibleForTesting; import com.android.server.telecom.LogUtils.EventTimer; +import com.android.server.telecom.flags.FeatureFlags; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; @@ -59,6 +69,8 @@ import java.util.function.Supplier; */ @VisibleForTesting public class Ringer { + private static final String TAG = "TelecomRinger"; + public interface AccessibilityManagerAdapter { boolean startFlashNotificationSequence(@NonNull Context context, @AccessibilityManager.FlashNotificationReason int reason); @@ -84,6 +96,16 @@ public class Ringer { // Used for test to notify the completion of RingerAttributes private CountDownLatch mAttributesLatch; + /** + * Delay to be used between consecutive vibrations when a non-repeating vibration effect is + * provided by the device. + * + * <p>If looking to customize the loop delay for a device's ring vibration, the desired repeat + * behavior should be encoded directly in the effect specification in the device configuration + * rather than changing the here (i.e. in `R.raw.default_ringtone_vibration_effect` resource). + */ + private static int DEFAULT_RING_VIBRATION_LOOP_DELAY_MS = 1000; + private static final long[] PULSE_PRIMING_PATTERN = {0,12,250,12,500}; // priming + interval private static final int[] PULSE_PRIMING_AMPLITUDE = {0,255,0,255,0}; // priming + interval @@ -96,9 +118,11 @@ public class Ringer { private static final int[] PULSE_RAMPING_AMPLITUDE = { 77,77,78,79,81,84,87,93,101,114,133,162,205,255,255,0}; - private static final long[] PULSE_PATTERN; + @VisibleForTesting + public static final long[] PULSE_PATTERN; - private static final int[] PULSE_AMPLITUDE; + @VisibleForTesting + public static final int[] PULSE_AMPLITUDE; private static final int RAMPING_RINGER_VIBRATION_DURATION = 5000; private static final int RAMPING_RINGER_DURATION = 10000; @@ -162,6 +186,7 @@ public class Ringer { private final InCallController mInCallController; private final VibrationEffectProxy mVibrationEffectProxy; private final boolean mIsHapticPlaybackSupportedByDevice; + private final FeatureFlags mFlags; /** * For unit testing purposes only; when set, {@link #startRinging(Call, boolean)} will complete * the future provided by the test using {@link #setBlockOnRingingFuture(CompletableFuture)}. @@ -207,7 +232,8 @@ public class Ringer { VibrationEffectProxy vibrationEffectProxy, InCallController inCallController, NotificationManager notificationManager, - AccessibilityManagerAdapter accessibilityManagerAdapter) { + AccessibilityManagerAdapter accessibilityManagerAdapter, + FeatureFlags featureFlags) { mLock = new Object(); mSystemSettingsUtil = systemSettingsUtil; @@ -223,18 +249,15 @@ public class Ringer { mNotificationManager = notificationManager; mAccessibilityManagerAdapter = accessibilityManagerAdapter; - if (mContext.getResources().getBoolean(R.bool.use_simple_vibration_pattern)) { - mDefaultVibrationEffect = mVibrationEffectProxy.createWaveform(SIMPLE_VIBRATION_PATTERN, - SIMPLE_VIBRATION_AMPLITUDE, REPEAT_SIMPLE_VIBRATION_AT); - } else { - mDefaultVibrationEffect = mVibrationEffectProxy.createWaveform(PULSE_PATTERN, - PULSE_AMPLITUDE, REPEAT_VIBRATION_AT); - } + mDefaultVibrationEffect = + loadDefaultRingVibrationEffect( + mContext, mVibrator, mVibrationEffectProxy, featureFlags); mIsHapticPlaybackSupportedByDevice = mSystemSettingsUtil.isHapticPlaybackSupported(mContext); mAudioManager = mContext.getSystemService(AudioManager.class); + mFlags = featureFlags; } @VisibleForTesting @@ -629,14 +652,21 @@ public class Ringer { Log.i(this, "shouldRingForContact: returning computation from DndCallFilter."); return !call.isCallSuppressedByDoNotDisturb(); } - final Uri contactUri = call.getHandle(); - final Bundle peopleExtras = new Bundle(); - if (contactUri != null) { - ArrayList<Person> personList = new ArrayList<>(); - personList.add(new Person.Builder().setUri(contactUri.toString()).build()); - peopleExtras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, personList); + Uri contactUri = call.getHandle(); + if (mFlags.telecomResolveHiddenDependencies()) { + if (contactUri == null) { + contactUri = Uri.EMPTY; + } + return mNotificationManager.matchesCallFilter(contactUri); + } else { + final Bundle peopleExtras = new Bundle(); + if (contactUri != null) { + ArrayList<Person> personList = new ArrayList<>(); + personList.add(new Person.Builder().setUri(contactUri.toString()).build()); + peopleExtras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, personList); + } + return mNotificationManager.matchesCallFilter(peopleExtras); } - return mNotificationManager.matchesCallFilter(peopleExtras); } private boolean hasExternalRinger(Call foregroundCall) { @@ -757,4 +787,65 @@ public class Ringer { return false; } } + + @Nullable + private static VibrationEffect loadSerializedDefaultRingVibration( + Resources resources, Vibrator vibrator) { + try { + InputStream vibrationInputStream = + resources.openRawResource( + com.android.internal.R.raw.default_ringtone_vibration_effect); + ParsedVibration parsedVibration = VibrationXmlParser + .parseDocument( + new InputStreamReader(vibrationInputStream, StandardCharsets.UTF_8)); + if (parsedVibration == null) { + Log.w(TAG, "Got null parsed default ring vibration effect."); + return null; + } + return parsedVibration.resolve(vibrator); + } catch (IOException | Resources.NotFoundException e) { + Log.e(TAG, e, "Error parsing default ring vibration effect."); + return null; + } + } + + private static VibrationEffect loadDefaultRingVibrationEffect( + Context context, + Vibrator vibrator, + VibrationEffectProxy vibrationEffectProxy, + FeatureFlags featureFlags) { + Resources resources = context.getResources(); + + if (resources.getBoolean(R.bool.use_simple_vibration_pattern)) { + Log.i(TAG, "Using simple default ring vibration."); + return createSimpleRingVibration(vibrationEffectProxy); + } + + if (featureFlags.useDeviceProvidedSerializedRingerVibration()) { + VibrationEffect parsedEffect = loadSerializedDefaultRingVibration(resources, vibrator); + if (parsedEffect != null) { + Log.i(TAG, "Using parsed default ring vibration."); + // Make the parsed effect repeating to make it vibrate continuously during ring. + // If the effect is already repeating, this API call is a no-op. + // Otherwise, it uses `DEFAULT_RING_VIBRATION_LOOP_DELAY_MS` when changing a + // non-repeating vibration to a repeating vibration. + // This is so that we ensure consecutive loops of the vibration play with some gap + // in between. + return parsedEffect.applyRepeatingIndefinitely( + /* wantRepeating= */ true, DEFAULT_RING_VIBRATION_LOOP_DELAY_MS); + } + // Fallback to the simple vibration if the serialized effect cannot be loaded. + return createSimpleRingVibration(vibrationEffectProxy); + } + + Log.i(TAG, "Using pulse default ring vibration."); + return vibrationEffectProxy.createWaveform( + PULSE_PATTERN, PULSE_AMPLITUDE, REPEAT_VIBRATION_AT); + } + + private static VibrationEffect createSimpleRingVibration( + VibrationEffectProxy vibrationEffectProxy) { + return vibrationEffectProxy.createWaveform(SIMPLE_VIBRATION_PATTERN, + SIMPLE_VIBRATION_AMPLITUDE, REPEAT_SIMPLE_VIBRATION_AT); + } } diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java index f33b18586..1dd68c91e 100644 --- a/src/com/android/server/telecom/TelecomServiceImpl.java +++ b/src/com/android/server/telecom/TelecomServiceImpl.java @@ -79,6 +79,7 @@ import com.android.internal.telecom.ICallEventCallback; import com.android.internal.telecom.ITelecomService; import com.android.internal.util.IndentingPrintWriter; import com.android.server.telecom.components.UserCallIntentProcessorFactory; +import com.android.server.telecom.flags.FeatureFlags; import com.android.server.telecom.settings.BlockedNumbersActivity; import com.android.server.telecom.voip.IncomingCallTransaction; import com.android.server.telecom.voip.OutgoingCallTransaction; @@ -88,6 +89,7 @@ import com.android.server.telecom.voip.VoipCallTransactionResult; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.lang.reflect.Method; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -1551,6 +1553,23 @@ public class TelecomServiceImpl { } mCallIntentProcessorAdapter.processIncomingCallIntent( mCallsManager, intent); + if (mFeatureFlags.earlyBindingToIncallService()) { + PhoneAccount account = + mPhoneAccountRegistrar.getPhoneAccountUnchecked( + phoneAccountHandle); + Bundle accountExtra = + account == null ? new Bundle() : account.getExtras(); + PackageManager packageManager = mContext.getPackageManager(); + // Start binding to InCallServices for wearable calls that do not + // require call filtering. This is to wake up default dialer earlier + // to mitigate InCallService binding latency. + if (packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH) + && accountExtra != null && accountExtra.getBoolean( + PhoneAccount.EXTRA_SKIP_CALL_FILTERING, + false)) { + mCallsManager.getInCallController().bindToServices(null); + } + } } finally { Binder.restoreCallingIdentity(token); } @@ -1967,6 +1986,11 @@ public class TelecomServiceImpl { pw.increaseIndent(); Analytics.dump(pw); pw.decreaseIndent(); + + pw.println("Flag Configurations: "); + pw.increaseIndent(); + reflectAndPrintFlagConfigs(pw); + pw.decreaseIndent(); } if (isTimeLineView) { Log.dumpEventsTimeline(pw); @@ -1976,6 +2000,28 @@ public class TelecomServiceImpl { } /** + * Print all feature flag configurations that Telecom is using for debugging purposes. + */ + private void reflectAndPrintFlagConfigs(IndentingPrintWriter pw) { + + try { + // Look away, a forbidden technique (reflection) is being used to allow us to get + // all flag configs without having to add them manually to this method. + Method[] methods = FeatureFlags.class.getMethods(); + if (methods.length == 0) { + pw.println("NONE"); + return; + } + for (Method m : methods) { + pw.println(m.getName() + "-> " + m.invoke(mFeatureFlags)); + } + } catch (Exception e) { + pw.println("[ERROR]"); + } + + } + + /** * @see android.telecom.TelecomManager#createManageBlockedNumbersIntent */ @Override @@ -2138,7 +2184,7 @@ public class TelecomServiceImpl { try { Log.i(this, "handleCallIntent: handling call intent"); mCallIntentProcessorAdapter.processOutgoingCallIntent(mContext, - mCallsManager, intent, callingPackage); + mCallsManager, intent, callingPackage, mFeatureFlags); } finally { Binder.restoreCallingIdentity(token); } @@ -2212,6 +2258,39 @@ public class TelecomServiceImpl { } /** + * A method intended for use in testing to query whether a particular non-ui inCallService + * is bound in a call. + * @param packageName of the service to query. + * @return whether it is bound or not. + */ + @Override + public boolean isNonUiInCallServiceBound(String packageName) { + Log.startSession("TCI.iNUICSB"); + try { + synchronized (mLock) { + enforceShellOnly(Binder.getCallingUid(), "isNonUiInCallServiceBound"); + if (!(mContext.checkCallingOrSelfPermission(READ_PHONE_STATE) + == PackageManager.PERMISSION_GRANTED) || + !(mContext.checkCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE) + == PackageManager.PERMISSION_GRANTED)) { + throw new SecurityException("isNonUiInCallServiceBound requires the" + + " READ_PHONE_STATE or READ_PRIVILEGED_PHONE_STATE permission"); + } + long token = Binder.clearCallingIdentity(); + try { + return mCallsManager + .getInCallController() + .isNonUiInCallServiceBound(packageName); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } finally { + Log.endSession(); + } + } + + /** * A method intended for use in testing to reset car mode at all priorities. * * Runs during setup to avoid cascading failures from failing car mode CTS. @@ -2478,6 +2557,7 @@ public class TelecomServiceImpl { private final TelecomSystem.SyncRoot mLock; private TransactionManager mTransactionManager; private final TransactionalServiceRepository mTransactionalServiceRepository; + private final FeatureFlags mFeatureFlags; public TelecomServiceImpl( Context context, @@ -2488,6 +2568,7 @@ public class TelecomServiceImpl { DefaultDialerCache defaultDialerCache, SubscriptionManagerAdapter subscriptionManagerAdapter, SettingsSecureAdapter settingsSecureAdapter, + FeatureFlags featureFlags, TelecomSystem.SyncRoot lock) { mContext = context; mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); @@ -2495,6 +2576,7 @@ public class TelecomServiceImpl { mPackageManager = mContext.getPackageManager(); mCallsManager = callsManager; + mFeatureFlags = featureFlags; mLock = lock; mPhoneAccountRegistrar = phoneAccountRegistrar; mUserCallIntentProcessorFactory = userCallIntentProcessorFactory; @@ -2707,6 +2789,7 @@ public class TelecomServiceImpl { int packageUid = -1; int callingUid = Binder.getCallingUid(); PackageManager pm; + long token = Binder.clearCallingIdentity(); try{ pm = mContext.createContextAsUser( UserHandle.getUserHandleForUid(callingUid), 0).getPackageManager(); @@ -2715,6 +2798,8 @@ public class TelecomServiceImpl { Log.i(this, "callingUidMatchesPackageManagerRecords:" + " createContextAsUser hit exception=[%s]", e.toString()); return false; + } finally { + Binder.restoreCallingIdentity(token); } if (pm != null) { try { diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java index 3686e86be..101cd2d09 100644 --- a/src/com/android/server/telecom/TelecomSystem.java +++ b/src/com/android/server/telecom/TelecomSystem.java @@ -45,8 +45,12 @@ import com.android.server.telecom.bluetooth.BluetoothDeviceManager; import com.android.server.telecom.bluetooth.BluetoothRouteManager; import com.android.server.telecom.bluetooth.BluetoothStateReceiver; import com.android.server.telecom.callfiltering.BlockedNumbersAdapter; +import com.android.server.telecom.callfiltering.CallFilterResultCallback; +import com.android.server.telecom.callfiltering.IncomingCallFilterGraph; +import com.android.server.telecom.callfiltering.IncomingCallFilterGraphProvider; import com.android.server.telecom.components.UserCallIntentProcessor; import com.android.server.telecom.components.UserCallIntentProcessorFactory; +import com.android.server.telecom.flags.FeatureFlags; import com.android.server.telecom.ui.AudioProcessingNotification; import com.android.server.telecom.ui.CallStreamingNotification; import com.android.server.telecom.ui.DisconnectedCallNotifier; @@ -224,7 +228,8 @@ public class TelecomSystem { Ringer.AccessibilityManagerAdapter accessibilityManagerAdapter, Executor asyncTaskExecutor, Executor asyncCallAudioTaskExecutor, - BlockedNumbersAdapter blockedNumbersAdapter) { + BlockedNumbersAdapter blockedNumbersAdapter, + FeatureFlags featureFlags) { mContext = context.getApplicationContext(); LogUtils.initLogging(mContext); android.telecom.Log.setLock(mLock); @@ -254,11 +259,13 @@ public class TelecomSystem { CallAudioCommunicationDeviceTracker(mContext); BluetoothDeviceManager bluetoothDeviceManager = new BluetoothDeviceManager(mContext, mContext.getSystemService(BluetoothManager.class).getAdapter(), - communicationDeviceTracker); + communicationDeviceTracker, featureFlags); BluetoothRouteManager bluetoothRouteManager = new BluetoothRouteManager(mContext, mLock, - bluetoothDeviceManager, new Timeouts.Adapter(), communicationDeviceTracker); + bluetoothDeviceManager, new Timeouts.Adapter(), + communicationDeviceTracker, featureFlags); BluetoothStateReceiver bluetoothStateReceiver = new BluetoothStateReceiver( - bluetoothDeviceManager, bluetoothRouteManager, communicationDeviceTracker); + bluetoothDeviceManager, bluetoothRouteManager, + communicationDeviceTracker, featureFlags); mContext.registerReceiver(bluetoothStateReceiver, BluetoothStateReceiver.INTENT_FILTER); communicationDeviceTracker.setBluetoothRouteManager(bluetoothRouteManager); @@ -268,7 +275,8 @@ public class TelecomSystem { mMissedCallNotifier = missedCallNotifierImplFactory .makeMissedCallNotifierImpl(mContext, mPhoneAccountRegistrar, defaultDialerCache, - deviceIdleControllerAdapter); + deviceIdleControllerAdapter, + featureFlags); DisconnectedCallNotifier.Factory disconnectedCallNotifierFactory = new DisconnectedCallNotifier.Default(); @@ -287,7 +295,7 @@ public class TelecomSystem { EmergencyCallHelper emergencyCallHelper) { return new InCallController(context, lock, callsManager, systemStateProvider, defaultDialerCache, timeoutsAdapter, emergencyCallHelper, - new CarModeTracker(), clockProxy); + new CarModeTracker(), clockProxy, featureFlags); } }; @@ -406,7 +414,9 @@ public class TelecomSystem { transactionManager, emergencyCallDiagnosticLogger, communicationDeviceTracker, - callStreamingNotification); + callStreamingNotification, + featureFlags, + IncomingCallFilterGraph::new); mIncomingCallNotifier = incomingCallNotifier; incomingCallNotifier.setCallsManagerProxy(new IncomingCallNotifier.CallsManagerProxy() { @@ -450,7 +460,7 @@ public class TelecomSystem { } mCallIntentProcessor = new CallIntentProcessor(mContext, mCallsManager, - defaultDialerCache); + defaultDialerCache, featureFlags); mTelecomBroadcastIntentProcessor = new TelecomBroadcastIntentProcessor( mContext, mCallsManager); @@ -474,6 +484,7 @@ public class TelecomSystem { defaultDialerCache, new TelecomServiceImpl.SubscriptionManagerAdapterImpl(), new TelecomServiceImpl.SettingsSecureAdapterImpl(), + featureFlags, mLock); } finally { Log.endSession(); diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java index 25aaad789..02ccef7a9 100644 --- a/src/com/android/server/telecom/TransactionalServiceWrapper.java +++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java @@ -451,7 +451,8 @@ public class TransactionalServiceWrapper implements @Override public void onError(CallException exception) { - Log.i(TAG, "onSetInactive: onError: with e=[%e]", exception); + Log.w(TAG, "onSetInactive: onError: e.code=[%d], e.msg=[%s]", + exception.getCode(), exception.getMessage()); } }); } finally { @@ -498,8 +499,9 @@ public class TransactionalServiceWrapper implements @Override public void onError(CallException exception) { - Log.i(TAG, "onCallStreamingStarted: onError: with e=[%e]", - exception); + Log.w(TAG, "onCallStreamingStarted: onError: " + + "e.code=[%d], e.msg=[%s]", + exception.getCode(), exception.getMessage()); stopCallStreaming(call); } } diff --git a/src/com/android/server/telecom/UserUtil.java b/src/com/android/server/telecom/UserUtil.java index a30440139..670ad3438 100644 --- a/src/com/android/server/telecom/UserUtil.java +++ b/src/com/android/server/telecom/UserUtil.java @@ -16,10 +16,19 @@ package com.android.server.telecom; +import android.app.admin.DevicePolicyManager; import android.content.Context; +import android.content.Intent; import android.content.pm.UserInfo; +import android.net.Uri; import android.os.UserHandle; import android.os.UserManager; +import android.telecom.Log; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; + +import com.android.server.telecom.components.ErrorDialogActivity; +import com.android.server.telecom.flags.FeatureFlags; public final class UserUtil { @@ -40,4 +49,90 @@ public final class UserUtil { UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle); return userInfo != null && userInfo.profileGroupId != userInfo.id; } + + public static void showErrorDialogForRestrictedOutgoingCall(Context context, + int stringId, String tag, String reason) { + final Intent intent = new Intent(context, ErrorDialogActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(ErrorDialogActivity.ERROR_MESSAGE_ID_EXTRA, stringId); + context.startActivityAsUser(intent, UserHandle.CURRENT); + Log.w(tag, "Rejecting non-emergency phone call because " + + reason); + } + + public static boolean hasOutgoingCallsUserRestriction(Context context, + UserHandle userHandle, Uri handle, boolean isSelfManaged, String tag) { + // Set handle for conference calls. Refer to {@link Connection#ADHOC_CONFERENCE_ADDRESS}. + if (handle == null) { + handle = Uri.parse("tel:conf-factory"); + } + + if(!isSelfManaged) { + // Check DISALLOW_OUTGOING_CALLS restriction. Note: We are skipping this + // check in a managed profile user because this check can always be bypassed + // by copying and pasting the phone number into the personal dialer. + if (!UserUtil.isManagedProfile(context, userHandle)) { + // Only emergency calls are allowed for users with the DISALLOW_OUTGOING_CALLS + // restriction. + if (!TelephonyUtil.shouldProcessAsEmergency(context, handle)) { + final UserManager userManager = + (UserManager) context.getSystemService(Context.USER_SERVICE); + if (userManager.hasBaseUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS, + userHandle)) { + String reason = "of DISALLOW_OUTGOING_CALLS restriction"; + showErrorDialogForRestrictedOutgoingCall(context, + R.string.outgoing_call_not_allowed_user_restriction, tag, reason); + return true; + } else if (userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS, + userHandle)) { + final DevicePolicyManager dpm = + context.getSystemService(DevicePolicyManager.class); + if (dpm == null) { + return true; + } + final Intent adminSupportIntent = dpm.createAdminSupportIntent( + UserManager.DISALLOW_OUTGOING_CALLS); + if (adminSupportIntent != null) { + context.startActivityAsUser(adminSupportIntent, userHandle); + } + return true; + } + } + } + } + return false; + } + + /** + * Gets the associated user for the given call. Note: this is applicable to all calls except + * outgoing calls as the associated user is already based off of the user placing the + * call. + * + * @param phoneAccountRegistrar + * @param currentUser Current user profile (this can either be the admin or a secondary/guest + * user). Note that work profile users fall under the admin user. + * @param targetPhoneAccount The phone account to retrieve the {@link UserHandle} from. + * @return current user if it isn't the admin or if the work profile is paused for the target + * phone account handle user, otherwise return the target phone account handle user. If the + * flag is disabled, return the legacy {@link UserHandle}. + */ + public static UserHandle getAssociatedUserForCall(boolean isAssociatedUserFlagEnabled, + PhoneAccountRegistrar phoneAccountRegistrar, UserHandle currentUser, + PhoneAccountHandle targetPhoneAccount) { + if (!isAssociatedUserFlagEnabled) { + return targetPhoneAccount.getUserHandle(); + } + // For multi-user phone accounts, associate the call with the profile receiving/placing + // the call. For SIM accounts (that are assigned to specific users), the user association + // will be placed on the target phone account handle user. + PhoneAccount account = phoneAccountRegistrar.getPhoneAccountUnchecked(targetPhoneAccount); + if (account != null) { + return account.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER) + ? currentUser + : targetPhoneAccount.getUserHandle(); + } + // If target phone account handle is null or account cannot be found, + // return the current user. + return currentUser; + } } diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java index b19bf818f..9ae58b33d 100644 --- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java +++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java @@ -27,7 +27,6 @@ import android.bluetooth.BluetoothStatusCodes; import android.content.Context; import android.media.AudioManager; import android.media.AudioDeviceInfo; -import android.media.audio.common.AudioDevice; import android.os.Bundle; import android.telecom.Log; import android.util.ArraySet; @@ -36,17 +35,18 @@ import android.util.LocalLog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; import com.android.server.telecom.CallAudioCommunicationDeviceTracker; +import com.android.server.telecom.flags.FeatureFlags; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.concurrent.Executor; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.concurrent.Executor; public class BluetoothDeviceManager { @@ -62,7 +62,8 @@ public class BluetoothDeviceManager { public void onGroupStatusChanged(int groupId, int groupStatus) {} @Override public void onGroupNodeAdded(BluetoothDevice device, int groupId) { - Log.i(this, device.getAddress() + " group added " + groupId); + Log.i(this, (device == null ? "device is null" : device.getAddress()) + + " group added " + groupId); if (device == null || groupId == BluetoothLeAudio.GROUP_ID_INVALID) { Log.w(this, "invalid parameter"); return; @@ -74,6 +75,8 @@ public class BluetoothDeviceManager { } @Override public void onGroupNodeRemoved(BluetoothDevice device, int groupId) { + Log.i(this, (device == null ? "device is null" : device.getAddress()) + + " group removed " + groupId); if (device == null || groupId == BluetoothLeAudio.GROUP_ID_INVALID) { Log.w(this, "invalid parameter"); return; @@ -89,7 +92,7 @@ public class BluetoothDeviceManager { new BluetoothProfile.ServiceListener() { @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { - Log.startSession("BMSL.oSC"); + Log.startSession("BPSL.oSC"); try { synchronized (mLock) { String logString; @@ -105,9 +108,13 @@ public class BluetoothDeviceManager { logString = "Got BluetoothLeAudio: " + mBluetoothLeAudioService; if (!mLeAudioCallbackRegistered) { - mBluetoothLeAudioService.registerCallback( - mExecutor, mLeAudioCallbacks); - mLeAudioCallbackRegistered = true; + try { + mBluetoothLeAudioService.registerCallback( + mExecutor, mLeAudioCallbacks); + mLeAudioCallbackRegistered = true; + } catch (IllegalStateException e) { + logString += ", but Bluetooth is down"; + } } } else { logString = "Connected to non-requested bluetooth service." + @@ -123,7 +130,7 @@ public class BluetoothDeviceManager { @Override public void onServiceDisconnected(int profile) { - Log.startSession("BMSL.oSD"); + Log.startSession("BPSL.oSD"); try { synchronized (mLock) { LinkedHashMap<String, BluetoothDevice> lostServiceDevices; @@ -205,9 +212,11 @@ public class BluetoothDeviceManager { private AudioManager mAudioManager; private Executor mExecutor; private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker; + private FeatureFlags mFeatureFlags; public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter, - CallAudioCommunicationDeviceTracker communicationDeviceTracker) { + CallAudioCommunicationDeviceTracker communicationDeviceTracker, + FeatureFlags featureFlags) { if (bluetoothAdapter != null) { mBluetoothAdapter = bluetoothAdapter; bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener, @@ -219,6 +228,7 @@ public class BluetoothDeviceManager { mAudioManager = context.getSystemService(AudioManager.class); mExecutor = context.getMainExecutor(); mCommunicationDeviceTracker = communicationDeviceTracker; + mFeatureFlags = featureFlags; } } @@ -443,7 +453,17 @@ public class BluetoothDeviceManager { } public void disconnectAudio() { - mCommunicationDeviceTracker.clearBtCommunicationDevice(); + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { + mCommunicationDeviceTracker.clearBtCommunicationDevice(); + disconnectSco(); + } else { + disconnectSco(); + clearLeAudioOrSpeakerCommunicationDevice(); + clearHearingAidOrSpeakerCommunicationDevice(); + } + } + + public void disconnectSco() { if (mBluetoothHeadset == null) { Log.w(this, "Trying to disconnect audio but no headset service exists."); } else { @@ -459,13 +479,9 @@ public class BluetoothDeviceManager { return mHearingAidSetAsCommunicationDevice; } - public void clearLeAudioCommunicationDevice() { + public void clearLeAudioOrSpeakerCommunicationDevice() { Log.i(this, "clearLeAudioCommunicationDevice: mLeAudioSetAsCommunicationDevice = " + mLeAudioSetAsCommunicationDevice + " device = " + mLeAudioDevice); - if (!mLeAudioSetAsCommunicationDevice) { - return; - } - mLeAudioSetAsCommunicationDevice = false; if (mLeAudioDevice != null) { mBluetoothRouteManager.onAudioLost(mLeAudioDevice); mLeAudioDevice = null; @@ -477,20 +493,22 @@ public class BluetoothDeviceManager { } AudioDeviceInfo audioDeviceInfo = mAudioManager.getCommunicationDevice(); - if (audioDeviceInfo != null && audioDeviceInfo.getType() - == AudioDeviceInfo.TYPE_BLE_HEADSET) { - mBluetoothRouteManager.onAudioLost(audioDeviceInfo.getAddress()); - mAudioManager.clearCommunicationDevice(); + if (audioDeviceInfo != null) { + if (audioDeviceInfo.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) { + Log.i(this, "clearLeAudioCommunicationDevice: clearing le audio"); + mBluetoothRouteManager.onAudioLost(audioDeviceInfo.getAddress()); + mAudioManager.clearCommunicationDevice(); + } else if (audioDeviceInfo.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { + Log.i(this, "clearLeAudioCommunicationDevice: clearing speaker"); + mAudioManager.clearCommunicationDevice(); + } } + mLeAudioSetAsCommunicationDevice = false; } - public void clearHearingAidCommunicationDevice() { + public void clearHearingAidOrSpeakerCommunicationDevice() { Log.i(this, "clearHearingAidCommunicationDevice: mHearingAidSetAsCommunicationDevice = " + mHearingAidSetAsCommunicationDevice); - if (!mHearingAidSetAsCommunicationDevice) { - return; - } - mHearingAidSetAsCommunicationDevice = false; if (mHearingAidDevice != null) { mBluetoothRouteManager.onAudioLost(mHearingAidDevice); mHearingAidDevice = null; @@ -502,10 +520,17 @@ public class BluetoothDeviceManager { } AudioDeviceInfo audioDeviceInfo = mAudioManager.getCommunicationDevice(); - if (audioDeviceInfo != null && audioDeviceInfo.getType() - == AudioDeviceInfo.TYPE_HEARING_AID) { - mAudioManager.clearCommunicationDevice(); + if (audioDeviceInfo != null) { + if (audioDeviceInfo.getType() == AudioDeviceInfo.TYPE_HEARING_AID) { + Log.i(this, "clearHearingAidCommunicationDevice: clearing hearing aid"); + mBluetoothRouteManager.onAudioLost(audioDeviceInfo.getAddress()); + mAudioManager.clearCommunicationDevice(); + } else if (audioDeviceInfo.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { + Log.i(this, "clearHearingAidCommunicationDevice: clearing speaker"); + mAudioManager.clearCommunicationDevice(); + } } + mHearingAidSetAsCommunicationDevice = false; } public boolean setLeAudioCommunicationDevice() { @@ -542,7 +567,7 @@ public class BluetoothDeviceManager { } // clear hearing aid communication device if set - clearHearingAidCommunicationDevice(); + clearHearingAidOrSpeakerCommunicationDevice(); // Turn BLE_OUT_HEADSET ON. boolean result = mAudioManager.setCommunicationDevice(bleHeadset); @@ -591,7 +616,7 @@ public class BluetoothDeviceManager { } // clear LE audio communication device if set - clearLeAudioCommunicationDevice(); + clearLeAudioOrSpeakerCommunicationDevice(); // Turn hearing aid ON. boolean result = mAudioManager.setCommunicationDevice(hearingAid); @@ -657,8 +682,10 @@ public class BluetoothDeviceManager { * Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that * will be audio switched to is available to be choose as communication device */ if (!switchingBtDevices) { - return mCommunicationDeviceTracker.setCommunicationDevice( - AudioDeviceInfo.TYPE_BLE_HEADSET, device); + return mFeatureFlags.callAudioCommunicationDeviceRefactor() ? + mCommunicationDeviceTracker.setCommunicationDevice( + AudioDeviceInfo.TYPE_BLE_HEADSET, device) + : setLeAudioCommunicationDevice(); } return true; } @@ -669,8 +696,10 @@ public class BluetoothDeviceManager { * Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that * will be audio switched to is available to be choose as communication device */ if (!switchingBtDevices) { - return mCommunicationDeviceTracker.setCommunicationDevice( - AudioDeviceInfo.TYPE_HEARING_AID, null); + return mFeatureFlags.callAudioCommunicationDeviceRefactor() ? + mCommunicationDeviceTracker.setCommunicationDevice( + AudioDeviceInfo.TYPE_HEARING_AID, null) + : setHearingAidCommunicationDevice(); } return true; } diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java index 91c03b6b1..ed2a72c0a 100644 --- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java +++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java @@ -17,11 +17,12 @@ package com.android.server.telecom.bluetooth; import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothHearingAid; -import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothLeAudio; +import android.bluetooth.BluetoothProfile; import android.content.Context; import android.media.AudioDeviceInfo; import android.os.Message; @@ -37,14 +38,14 @@ import com.android.internal.util.StateMachine; import com.android.server.telecom.CallAudioCommunicationDeviceTracker; import com.android.server.telecom.TelecomSystem; import com.android.server.telecom.Timeouts; +import com.android.server.telecom.flags.FeatureFlags; -import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -134,11 +135,12 @@ public class BluetoothRouteManager extends StateMachine { @Override public void enter() { BluetoothDevice erroneouslyConnectedDevice = getBluetoothAudioConnectedDevice(); - if (erroneouslyConnectedDevice != null) { + if (erroneouslyConnectedDevice != null && + !erroneouslyConnectedDevice.equals(mHearingAidActiveDeviceCache)) { Log.w(LOG_TAG, "Entering AudioOff state but device %s appears to be connected. " + "Switching to audio-on state for that device.", erroneouslyConnectedDevice); // change this to just transition to the new audio on state - transitionToActualState(); + transitionToActualState(null /* excludeAddress */); } cleanupStatesForDisconnectedDevices(); if (mListener != null) { @@ -252,6 +254,27 @@ public class BluetoothRouteManager extends StateMachine { SomeArgs args = (SomeArgs) msg.obj; String address = (String) args.arg2; boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address); + + if (switchingBtDevices == true) { // check if it is an hearing aid pair + BluetoothAdapter bluetoothAdapter = mDeviceManager.getBluetoothAdapter(); + if (bluetoothAdapter != null) { + List<BluetoothDevice> activeHearingAids = + bluetoothAdapter.getActiveDevices(BluetoothProfile.HEARING_AID); + for (BluetoothDevice hearingAid : activeHearingAids) { + if (hearingAid != null) { + String hearingAidAddress = hearingAid.getAddress(); + if (hearingAidAddress != null) { + if (hearingAidAddress.equals(address) || + hearingAidAddress.equals(mDeviceAddress)) { + switchingBtDevices = false; + break; + } + } + } + } + + } + } try { switch (msg.what) { case NEW_DEVICE_CONNECTED: @@ -261,7 +284,7 @@ public class BluetoothRouteManager extends StateMachine { case LOST_DEVICE: removeDevice((String) args.arg2); if (Objects.equals(address, mDeviceAddress)) { - transitionToActualState(); + transitionToActualState(null /* excludeAddress */); } break; case CONNECT_BT: @@ -301,7 +324,7 @@ public class BluetoothRouteManager extends StateMachine { case CONNECTION_TIMEOUT: Log.i(LOG_TAG, "Connection with device %s timed out.", mDeviceAddress); - transitionToActualState(); + transitionToActualState(null /* excludeAddress */); break; case BT_AUDIO_IS_ON: if (Objects.equals(mDeviceAddress, address)) { @@ -318,7 +341,7 @@ public class BluetoothRouteManager extends StateMachine { if (Objects.equals(mDeviceAddress, address) || address == null) { Log.i(LOG_TAG, "Connection with device %s failed.", mDeviceAddress); - transitionToActualState(); + transitionToActualState(address); } else { Log.w(LOG_TAG, "Got BT lost message for device %s while" + " connecting to %s.", address, mDeviceAddress); @@ -378,7 +401,7 @@ public class BluetoothRouteManager extends StateMachine { case LOST_DEVICE: removeDevice((String) args.arg2); if (Objects.equals(address, mDeviceAddress)) { - transitionToActualState(); + transitionToActualState(null /* excludeAddress */); } break; case CONNECT_BT: @@ -393,8 +416,13 @@ public class BluetoothRouteManager extends StateMachine { String actualAddress = connectBtAudio(address, true /* switchingBtDevices*/); if (actualAddress != null) { - transitionTo(getConnectingStateForAddress(address, - "AudioConnected/CONNECT_BT")); + if (mFeatureFlags.useActualAddressToEnterConnectingState()) { + transitionTo(getConnectingStateForAddress(actualAddress, + "AudioConnected/CONNECT_BT")); + } else { + transitionTo(getConnectingStateForAddress(address, + "AudioConnected/CONNECT_BT")); + } } else { Log.w(LOG_TAG, "Tried to connect to %s but failed" + " to connect to any BT device.", (String) args.arg2); @@ -435,7 +463,7 @@ public class BluetoothRouteManager extends StateMachine { case BT_AUDIO_LOST: if (Objects.equals(mDeviceAddress, address) || address == null) { Log.i(LOG_TAG, "BT connection with device %s lost.", mDeviceAddress); - transitionToActualState(); + transitionToActualState(address); } else { Log.w(LOG_TAG, "Got BT lost message for device %s while" + " connected to %s.", address, mDeviceAddress); @@ -472,10 +500,12 @@ public class BluetoothRouteManager extends StateMachine { private BluetoothDevice mLeAudioActiveDeviceCache = null; private BluetoothDevice mMostRecentlyReportedActiveDevice = null; private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker; + private FeatureFlags mFeatureFlags; public BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock, BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter, - CallAudioCommunicationDeviceTracker communicationDeviceTracker) { + CallAudioCommunicationDeviceTracker communicationDeviceTracker, + FeatureFlags featureFlags) { super(BluetoothRouteManager.class.getSimpleName()); mContext = context; mLock = lock; @@ -483,6 +513,7 @@ public class BluetoothRouteManager extends StateMachine { mDeviceManager.setBluetoothRouteManager(this); mTimeoutsAdapter = timeoutsAdapter; mCommunicationDeviceTracker = communicationDeviceTracker; + mFeatureFlags = featureFlags; mAudioOffState = new AudioOffState(); addState(mAudioOffState); @@ -626,14 +657,22 @@ public class BluetoothRouteManager extends StateMachine { if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) { mLeAudioActiveDeviceCache = device; if (device == null) { - mCommunicationDeviceTracker.clearCommunicationDevice( - AudioDeviceInfo.TYPE_BLE_HEADSET); + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { + mCommunicationDeviceTracker.clearCommunicationDevice( + AudioDeviceInfo.TYPE_BLE_HEADSET); + } else { + mDeviceManager.clearLeAudioOrSpeakerCommunicationDevice(); + } } } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID) { mHearingAidActiveDeviceCache = device; if (device == null) { - mCommunicationDeviceTracker.clearCommunicationDevice( - AudioDeviceInfo.TYPE_HEARING_AID); + if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { + mCommunicationDeviceTracker.clearCommunicationDevice( + AudioDeviceInfo.TYPE_HEARING_AID); + } else { + mDeviceManager.clearHearingAidOrSpeakerCommunicationDevice(); + } } } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEADSET) { mHfpActiveDeviceCache = device; @@ -670,6 +709,33 @@ public class BluetoothRouteManager extends StateMachine { return mDeviceManager.getUniqueConnectedDevices(); } + public boolean isWatch(BluetoothDevice device) { + if (device == null) { + Log.i(this, "isWatch: device is null. Returning false"); + return false; + } + + BluetoothClass deviceClass = device.getBluetoothClass(); + if (deviceClass != null && deviceClass.getDeviceClass() + == BluetoothClass.Device.WEARABLE_WRIST_WATCH) { + Log.i(this, "isWatch: bluetooth class component is a WEARABLE_WRIST_WATCH."); + return true; + } + + // Check metadata + byte[] deviceType = device.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE); + if (deviceType == null) { + return false; + } + String deviceTypeStr = new String(deviceType); + if (deviceTypeStr.equals(BluetoothDevice.DEVICE_TYPE_WATCH)) { + Log.i(this, "isWatch: bluetooth device type is DEVICE_TYPE_WATCH."); + return true; + } + + return false; + } + private String connectBtAudio(String address, boolean switchingBtDevices) { return connectBtAudio(address, 0, switchingBtDevices); } @@ -699,10 +765,19 @@ public class BluetoothRouteManager extends StateMachine { ? address : getActiveDeviceAddress(); if (actualAddress == null) { Log.i(this, "No device specified and BT stack has no active device." - + " Using arbitrary device"); + + " Using arbitrary device - except watch"); if (deviceList.size() > 0) { - actualAddress = deviceList.iterator().next().getAddress(); - } else { + for (BluetoothDevice device : deviceList) { + if (mFeatureFlags.ignoreAutoRouteToWatchDevice() && isWatch(device)) { + Log.i(this, "Skipping a watch device: " + device); + continue; + } + actualAddress = device.getAddress(); + break; + } + } + + if (actualAddress == null) { Log.i(this, "No devices available at all. Not connecting."); return null; } @@ -717,7 +792,7 @@ public class BluetoothRouteManager extends StateMachine { actualAddress)) { Log.i(this, "trying to connect to already connected device -- skipping connection" + " and going into the actual connected state."); - transitionToActualState(); + transitionToActualState(null /* excludeAddress */); return null; } @@ -753,9 +828,10 @@ public class BluetoothRouteManager extends StateMachine { return null; } - private void transitionToActualState() { + private void transitionToActualState(String excludeAddress) { BluetoothDevice possiblyAlreadyConnectedDevice = getBluetoothAudioConnectedDevice(); - if (possiblyAlreadyConnectedDevice != null) { + if (possiblyAlreadyConnectedDevice != null + && !possiblyAlreadyConnectedDevice.getAddress().equals(excludeAddress)) { Log.i(LOG_TAG, "Device %s is already connected; going to AudioConnected.", possiblyAlreadyConnectedDevice); transitionTo(getConnectedStateForAddress( @@ -804,23 +880,37 @@ public class BluetoothRouteManager extends StateMachine { } } + boolean isHearingAidSetForCommunication = + mFeatureFlags.callAudioCommunicationDeviceRefactor() + ? mCommunicationDeviceTracker.isAudioDeviceSetForType( + AudioDeviceInfo.TYPE_HEARING_AID) + : mDeviceManager.isHearingAidSetAsCommunicationDevice(); if (bluetoothHearingAid != null) { - if (mCommunicationDeviceTracker.isAudioDeviceSetForType( - AudioDeviceInfo.TYPE_HEARING_AID)) { - for (BluetoothDevice device : bluetoothAdapter.getActiveDevices( - BluetoothProfile.HEARING_AID)) { - if (device != null) { - hearingAidActiveDevice = device; - activeDevices++; - break; + if (isHearingAidSetForCommunication) { + List<BluetoothDevice> hearingAidsActiveDevices = bluetoothAdapter.getActiveDevices( + BluetoothProfile.HEARING_AID); + if (hearingAidsActiveDevices.contains(mHearingAidActiveDeviceCache)) { + hearingAidActiveDevice = mHearingAidActiveDeviceCache; + activeDevices++; + } else { + for (BluetoothDevice device : hearingAidsActiveDevices) { + if (device != null) { + hearingAidActiveDevice = device; + activeDevices++; + break; + } } } } } + boolean isLeAudioSetForCommunication = + mFeatureFlags.callAudioCommunicationDeviceRefactor() + ? mCommunicationDeviceTracker.isAudioDeviceSetForType( + AudioDeviceInfo.TYPE_BLE_HEADSET) + : mDeviceManager.isLeAudioCommunicationDevice(); if (bluetoothLeAudio != null) { - if (mCommunicationDeviceTracker.isAudioDeviceSetForType( - AudioDeviceInfo.TYPE_BLE_HEADSET)) { + if (isLeAudioSetForCommunication) { for (BluetoothDevice device : bluetoothAdapter.getActiveDevices( BluetoothProfile.LE_AUDIO)) { if (device != null) { diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java index ec4f2636e..d2521ac72 100644 --- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java +++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java @@ -33,6 +33,7 @@ import android.telecom.Logging.Session; import com.android.internal.os.SomeArgs; import com.android.server.telecom.CallAudioCommunicationDeviceTracker; +import com.android.server.telecom.flags.FeatureFlags; import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON; import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST; @@ -59,6 +60,7 @@ public class BluetoothStateReceiver extends BroadcastReceiver { private final BluetoothRouteManager mBluetoothRouteManager; private final BluetoothDeviceManager mBluetoothDeviceManager; private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker; + private FeatureFlags mFeatureFlags; public void onReceive(Context context, Intent intent) { Log.startSession("BSR.oR"); @@ -208,19 +210,27 @@ public class BluetoothStateReceiver extends BroadcastReceiver { /* In Le Audio case, once device got Active, the Telecom needs to make sure it * is set as communication device before we can say that BT_AUDIO_IS_ON */ + boolean isLeAudioSetForCommunication = + mFeatureFlags.callAudioCommunicationDeviceRefactor() + ? mCommunicationDeviceTracker.setCommunicationDevice( + AudioDeviceInfo.TYPE_BLE_HEADSET, device) + : mBluetoothDeviceManager.setLeAudioCommunicationDevice(); if ((!usePreferredAudioProfile || preferredDuplexProfile == BluetoothProfile.LE_AUDIO) - && !mCommunicationDeviceTracker.setCommunicationDevice( - AudioDeviceInfo.TYPE_BLE_HEADSET, device)) { + && !isLeAudioSetForCommunication) { Log.w(LOG_TAG, "Device %s cannot be use as LE audio communication device.", device); return; } } else { + boolean isHearingAidSetForCommunication = + mFeatureFlags.callAudioCommunicationDeviceRefactor() + ? mCommunicationDeviceTracker.setCommunicationDevice( + AudioDeviceInfo.TYPE_HEARING_AID, null) + : mBluetoothDeviceManager.setHearingAidCommunicationDevice(); /* deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID */ - if (!mCommunicationDeviceTracker.setCommunicationDevice( - AudioDeviceInfo.TYPE_HEARING_AID, null)) { + if (!isHearingAidSetForCommunication) { Log.w(LOG_TAG, "Device %s cannot be use as hearing aid communication device.", device); @@ -238,10 +248,12 @@ public class BluetoothStateReceiver extends BroadcastReceiver { public BluetoothStateReceiver(BluetoothDeviceManager deviceManager, BluetoothRouteManager routeManager, - CallAudioCommunicationDeviceTracker communicationDeviceTracker) { + CallAudioCommunicationDeviceTracker communicationDeviceTracker, + FeatureFlags featureFlags) { mBluetoothDeviceManager = deviceManager; mBluetoothRouteManager = routeManager; mCommunicationDeviceTracker = communicationDeviceTracker; + mFeatureFlags = featureFlags; } public void setIsInCall(boolean isInCall) { diff --git a/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java b/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java index 36f20775d..64060c870 100644 --- a/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java +++ b/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java @@ -126,6 +126,7 @@ public class BlockCheckerFilter extends CallFilter { .setShouldReject(true) .setShouldAddToCallLog(true) .setShouldShowNotification(false) + .setShouldSilence(true) .setCallBlockReason(getBlockReason(blockStatus)) .setCallScreeningAppName(null) .setCallScreeningComponentName(null) diff --git a/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java new file mode 100644 index 000000000..1501280de --- /dev/null +++ b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.telecom.callfiltering; + +import android.content.Context; + +import com.android.server.telecom.Call; +import com.android.server.telecom.TelecomSystem; +import com.android.server.telecom.Timeouts; + +/** + * Interface to provide a {@link IncomingCallFilterGraph}. This class serve for unit test purpose + * to mock an incoming call filter graph in test code. + */ +public interface IncomingCallFilterGraphProvider { + + + /** + * Provide a {@link IncomingCallFilterGraph} + * @param call The call for the filters. + * @param listener Callback object to trigger when filtering is done. + * @param context An android context. + * @param timeoutsAdapter Adapter to provide timeout value for call filtering. + * @param lock Telecom lock. + * @return + */ + IncomingCallFilterGraph createGraph(Call call, CallFilterResultCallback listener, + Context context, + Timeouts.Adapter timeoutsAdapter, TelecomSystem.SyncRoot lock); +} diff --git a/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java index 963e92317..05e73d544 100644 --- a/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java +++ b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java @@ -175,6 +175,20 @@ public class CallRedirectionProcessor implements CallRedirectionCallback { Log.endSession(); } } + + @Override + public void onBindingDied(ComponentName componentName) { + // Make sure we unbind the service if binding died to avoid background stating + // activity leaks + Log.startSession("CRSC.oBD"); + try { + synchronized (mTelecomLock) { + finishCallRedirection(); + } + } finally { + Log.endSession(); + } + } } private class CallRedirectionAdapter extends ICallRedirectionAdapter.Stub { diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java index 90a683fc8..9287d33fd 100644 --- a/src/com/android/server/telecom/components/TelecomService.java +++ b/src/com/android/server/telecom/components/TelecomService.java @@ -45,6 +45,7 @@ import com.android.server.telecom.ConnectionServiceFocusManager; import com.android.server.telecom.ContactsAsyncHelper; import com.android.server.telecom.DefaultDialerCache; import com.android.server.telecom.DeviceIdleControllerAdapter; +import com.android.server.telecom.flags.FeatureFlags; import com.android.server.telecom.HeadsetMediaButton; import com.android.server.telecom.HeadsetMediaButtonFactory; import com.android.server.telecom.InCallWakeLockControllerFactory; @@ -61,6 +62,7 @@ import com.android.server.telecom.TelecomSystem; import com.android.server.telecom.TelecomWakeLock; import com.android.server.telecom.Timeouts; import com.android.server.telecom.callfiltering.BlockedNumbersAdapter; +import com.android.server.telecom.flags.FeatureFlagsImpl; import com.android.server.telecom.settings.BlockedNumbersUtil; import com.android.server.telecom.ui.IncomingCallNotifier; import com.android.server.telecom.ui.MissedCallNotifierImpl; @@ -115,10 +117,11 @@ public class TelecomService extends Service implements TelecomSystem.Component { Context context, PhoneAccountRegistrar phoneAccountRegistrar, DefaultDialerCache defaultDialerCache, - DeviceIdleControllerAdapter idleControllerAdapter) { + DeviceIdleControllerAdapter idleControllerAdapter, + FeatureFlags featureFlags) { return new MissedCallNotifierImpl(context, phoneAccountRegistrar, defaultDialerCache, - idleControllerAdapter); + idleControllerAdapter, featureFlags); } }, new CallerInfoAsyncQueryFactory() { @@ -230,7 +233,8 @@ public class TelecomService extends Service implements TelecomSystem.Component { BlockedNumbersUtil.updateEmergencyCallNotification(context, showNotification); } - })); + }, + new FeatureFlagsImpl())); } } diff --git a/src/com/android/server/telecom/components/UserCallIntentProcessor.java b/src/com/android/server/telecom/components/UserCallIntentProcessor.java index a4602c1ca..41232c220 100755 --- a/src/com/android/server/telecom/components/UserCallIntentProcessor.java +++ b/src/com/android/server/telecom/components/UserCallIntentProcessor.java @@ -105,47 +105,17 @@ public class UserCallIntentProcessor { handle = Uri.fromParts(PhoneAccount.SCHEME_SIP, uriString, null); } - if(!isSelfManaged) { - // Check DISALLOW_OUTGOING_CALLS restriction. Note: We are skipping this - // check in a managed profile user because this check can always be bypassed - // by copying and pasting the phone number into the personal dialer. - if (!UserUtil.isManagedProfile(mContext, mUserHandle)) { - // Only emergency calls are allowed for users with the DISALLOW_OUTGOING_CALLS - // restriction. - if (!TelephonyUtil.shouldProcessAsEmergency(mContext, handle)) { - final UserManager userManager = - (UserManager) mContext.getSystemService(Context.USER_SERVICE); - if (userManager.hasBaseUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS, - mUserHandle)) { - showErrorDialogForRestrictedOutgoingCall(mContext, - R.string.outgoing_call_not_allowed_user_restriction); - Log.w(this, "Rejecting non-emergency phone call " - + "due to DISALLOW_OUTGOING_CALLS restriction"); - return; - } else if (userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS, - mUserHandle)) { - final DevicePolicyManager dpm = - mContext.getSystemService(DevicePolicyManager.class); - if (dpm == null) { - return; - } - final Intent adminSupportIntent = dpm.createAdminSupportIntent( - UserManager.DISALLOW_OUTGOING_CALLS); - if (adminSupportIntent != null) { - mContext.startActivity(adminSupportIntent); - } - return; - } - } - } - } + if (UserUtil.hasOutgoingCallsUserRestriction(mContext, mUserHandle, + handle, isSelfManaged, UserCallIntentProcessor.class.getCanonicalName())) { + return; + } if (!isSelfManaged && !canCallNonEmergency && !TelephonyUtil.shouldProcessAsEmergency(mContext, handle)) { - showErrorDialogForRestrictedOutgoingCall(mContext, - R.string.outgoing_call_not_allowed_no_permission); - Log.w(this, "Rejecting non-emergency phone call because " - + android.Manifest.permission.CALL_PHONE + " permission is not granted."); + String reason = android.Manifest.permission.CALL_PHONE + " permission is not granted."; + UserUtil.showErrorDialogForRestrictedOutgoingCall(mContext, + R.string.outgoing_call_not_allowed_no_permission, + this.getClass().getCanonicalName(), reason); return; } @@ -187,11 +157,4 @@ public class UserCallIntentProcessor { } return true; } - - private static void showErrorDialogForRestrictedOutgoingCall(Context context, int stringId) { - final Intent intent = new Intent(context, ErrorDialogActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(ErrorDialogActivity.ERROR_MESSAGE_ID_EXTRA, stringId); - context.startActivityAsUser(intent, UserHandle.CURRENT); - } } diff --git a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java index 6b97f970a..25ce0ca5a 100644 --- a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java +++ b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java @@ -20,6 +20,7 @@ import static android.Manifest.permission.READ_PHONE_STATE; import static android.app.admin.DevicePolicyResources.Strings.Telecomm.NOTIFICATION_MISSED_WORK_CALL_TITLE; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.BroadcastOptions; import android.app.Notification; import android.app.NotificationManager; @@ -42,6 +43,7 @@ import android.os.AsyncTask; import android.os.Binder; import android.os.Bundle; import android.os.UserHandle; +import android.provider.CallLog; import android.provider.CallLog.Calls; import android.telecom.CallerInfo; import android.telecom.Log; @@ -62,6 +64,7 @@ import com.android.server.telecom.CallsManagerListenerBase; import com.android.server.telecom.Constants; import com.android.server.telecom.DefaultDialerCache; import com.android.server.telecom.DeviceIdleControllerAdapter; +import com.android.server.telecom.flags.FeatureFlags; import com.android.server.telecom.MissedCallNotifier; import com.android.server.telecom.PhoneAccountRegistrar; import com.android.server.telecom.R; @@ -87,7 +90,8 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements MissedCallNotifier makeMissedCallNotifierImpl(Context context, PhoneAccountRegistrar phoneAccountRegistrar, DefaultDialerCache defaultDialerCache, - DeviceIdleControllerAdapter deviceIdleControllerAdapter); + DeviceIdleControllerAdapter deviceIdleControllerAdapter, + FeatureFlags featureFlags); } public interface NotificationBuilderFactory { @@ -141,19 +145,22 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements private final Map<UserHandle, Integer> mMissedCallCounts; private Set<UserHandle> mUsersToLoadAfterBootComplete = new ArraySet<>(); + private FeatureFlags mFeatureFlags; public MissedCallNotifierImpl(Context context, PhoneAccountRegistrar phoneAccountRegistrar, DefaultDialerCache defaultDialerCache, - DeviceIdleControllerAdapter deviceIdleControllerAdapter) { + DeviceIdleControllerAdapter deviceIdleControllerAdapter, + FeatureFlags featureFlags) { this(context, phoneAccountRegistrar, defaultDialerCache, - new DefaultNotificationBuilderFactory(), deviceIdleControllerAdapter); + new DefaultNotificationBuilderFactory(), deviceIdleControllerAdapter, featureFlags); } public MissedCallNotifierImpl(Context context, PhoneAccountRegistrar phoneAccountRegistrar, DefaultDialerCache defaultDialerCache, NotificationBuilderFactory notificationBuilderFactory, - DeviceIdleControllerAdapter deviceIdleControllerAdapter) { + DeviceIdleControllerAdapter deviceIdleControllerAdapter, + FeatureFlags featureFlags) { mContext = context; mPhoneAccountRegistrar = phoneAccountRegistrar; mNotificationManager = @@ -163,6 +170,7 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements mNotificationBuilderFactory = notificationBuilderFactory; mMissedCallCounts = new ArrayMap<>(); + mFeatureFlags = featureFlags; } /** Clears missed call notification and marks the call log's missed calls as read. */ @@ -261,17 +269,17 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements } private void sendNotificationThroughDefaultDialer(String dialerPackage, CallInfo callInfo, - UserHandle userHandle, int missedCallCount) { + UserHandle userHandle, int missedCallCount, @Nullable Uri uri) { Intent intent = getShowMissedCallIntentForDefaultDialer(dialerPackage) .setFlags(Intent.FLAG_RECEIVER_FOREGROUND) .putExtra(TelecomManager.EXTRA_CLEAR_MISSED_CALLS_INTENT, createClearMissedCallsPendingIntent(userHandle)) .putExtra(TelecomManager.EXTRA_NOTIFICATION_COUNT, missedCallCount) + .putExtra(TelecomManager.EXTRA_CALL_LOG_URI, uri) .putExtra(TelecomManager.EXTRA_NOTIFICATION_PHONE_NUMBER, callInfo == null ? null : callInfo.getPhoneNumber()) .putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, callInfo == null ? null : callInfo.getPhoneAccountHandle()); - if (missedCallCount == 1 && callInfo != null) { final Uri handleUri = callInfo.getHandle(); String handle = handleUri == null ? null : handleUri.getSchemeSpecificPart(); @@ -295,7 +303,7 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements * @param callInfo The missed call. */ @Override - public void showMissedCallNotification(@NonNull CallInfo callInfo) { + public void showMissedCallNotification(@NonNull CallInfo callInfo, @Nullable Uri uri) { final PhoneAccountHandle phoneAccountHandle = callInfo.getPhoneAccountHandle(); final PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(phoneAccountHandle); @@ -306,10 +314,11 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements } else { userHandle = phoneAccountHandle.getUserHandle(); } - showMissedCallNotification(callInfo, userHandle); + showMissedCallNotification(callInfo, userHandle, uri); } - private void showMissedCallNotification(@NonNull CallInfo callInfo, UserHandle userHandle) { + private void showMissedCallNotification(@NonNull CallInfo callInfo, UserHandle userHandle, + @Nullable Uri uri) { int missedCallCounts; synchronized (mMissedCallCountsLock) { Integer currentCount = mMissedCallCounts.get(userHandle); @@ -324,7 +333,7 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements String dialerPackage = getDefaultDialerPackage(userHandle); if (shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) { sendNotificationThroughDefaultDialer(dialerPackage, callInfo, userHandle, - missedCallCounts); + missedCallCounts, uri); return; } @@ -446,7 +455,7 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements String dialerPackage = getDefaultDialerPackage(userHandle); if (shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) { sendNotificationThroughDefaultDialer(dialerPackage, null, userHandle, - 0 /* missedCallCount */); + /* missedCallCount= */ 0, /* uri= */ null); return; } @@ -631,6 +640,13 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements while (cursor.moveToNext()) { // Get data about the missed call from the cursor final String handleString = cursor.getString(CALL_LOG_COLUMN_NUMBER); + final Uri uri; + if (mFeatureFlags.addCallUriForMissedCalls()){ + uri = Calls.CONTENT_URI.buildUpon().appendPath( + Long.toString(cursor.getInt(CALL_LOG_COLUMN_ID))).build(); + }else{ + uri = null; + } final int presentation = cursor.getInt(CALL_LOG_COLUMN_NUMBER_PRESENTATION); final long date = cursor.getLong(CALL_LOG_COLUMN_DATE); @@ -663,7 +679,8 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements // null, just show the notification. CallInfo callInfo = callInfoFactory.makeCallInfo( info, null, handle, date); - showMissedCallNotification(callInfo, userHandle); + showMissedCallNotification(callInfo, userHandle, + /* uri= */ uri); } } @@ -678,7 +695,8 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements } CallInfo callInfo = callInfoFactory.makeCallInfo( info, null, handle, date); - showMissedCallNotification(callInfo, userHandle); + showMissedCallNotification(callInfo, userHandle, + /* uri= */ uri); } } ); diff --git a/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java b/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java new file mode 100644 index 000000000..b17dedd44 --- /dev/null +++ b/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.telecom.voip; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.telecom.Call; +import com.android.server.telecom.CallsManager; + +import android.telecom.DisconnectCause; +import android.telecom.Log; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; + +/** + * VerifyCallStateChangeTransaction is a transaction that verifies a CallState change and has + * the ability to disconnect if the CallState is not changed within the timeout window. + * <p> + * Note: This transaction has a timeout of 2 seconds. + */ +public class VerifyCallStateChangeTransaction extends VoipCallTransaction { + private static final String TAG = VerifyCallStateChangeTransaction.class.getSimpleName(); + public static final int FAILURE_CODE = 0; + public static final int SUCCESS_CODE = 1; + public static final int TIMEOUT_SECONDS = 2; + private final Call mCall; + private final CallsManager mCallsManager; + private final int mTargetCallState; + private final boolean mShouldDisconnectUponFailure; + private final CompletableFuture<Integer> mCallStateOrTimeoutResult = new CompletableFuture<>(); + private final CompletableFuture<VoipCallTransactionResult> mTransactionResult = + new CompletableFuture<>(); + + @VisibleForTesting + public Call.CallStateListener mCallStateListenerImpl = new Call.CallStateListener() { + @Override + public void onCallStateChanged(int newCallState) { + Log.d(TAG, "newState=[%d], expectedState=[%d]", newCallState, mTargetCallState); + if (newCallState == mTargetCallState) { + mCallStateOrTimeoutResult.complete(SUCCESS_CODE); + } + // NOTE:: keep listening to the call state until the timeout is reached. It's possible + // another call state is reached in between... + } + }; + + public VerifyCallStateChangeTransaction(CallsManager callsManager, Call call, + int targetCallState, boolean shouldDisconnectUponFailure) { + super(callsManager.getLock()); + mCallsManager = callsManager; + mCall = call; + mTargetCallState = targetCallState; + mShouldDisconnectUponFailure = shouldDisconnectUponFailure; + } + + @Override + public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) { + Log.d(TAG, "processTransaction:"); + // It's possible the Call is already in the expected call state + if (isNewCallStateTargetCallState()) { + mTransactionResult.complete( + new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED, + TAG)); + return mTransactionResult; + } + initCallStateListenerOnTimeout(); + // At this point, the mCallStateOrTimeoutResult has been completed. There are 2 scenarios: + // (1) newCallState == targetCallState --> the transaction is successful + // (2) timeout is reached --> evaluate the current call state and complete the t accordingly + // also need to do cleanup for the transaction + evaluateCallStateUponChangeOrTimeout(); + + return mTransactionResult; + } + + private boolean isNewCallStateTargetCallState() { + return mCall.getState() == mTargetCallState; + } + + private void initCallStateListenerOnTimeout() { + mCall.addCallStateListener(mCallStateListenerImpl); + mCallStateOrTimeoutResult.completeOnTimeout(FAILURE_CODE, TIMEOUT_SECONDS, + TimeUnit.SECONDS); + } + + private void evaluateCallStateUponChangeOrTimeout() { + mCallStateOrTimeoutResult.thenAcceptAsync((result) -> { + Log.i(TAG, "processTransaction: thenAcceptAsync: result=[%s]", result); + mCall.removeCallStateListener(mCallStateListenerImpl); + if (isNewCallStateTargetCallState()) { + mTransactionResult.complete( + new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED, + TAG)); + } else { + maybeDisconnectCall(); + mTransactionResult.complete( + new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED, + TAG)); + } + }).exceptionally(exception -> { + Log.i(TAG, "hit exception=[%s] while completing future", exception); + mTransactionResult.complete( + new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED, + TAG)); + return null; + }); + } + + private void maybeDisconnectCall() { + if (mShouldDisconnectUponFailure) { + mCallsManager.markCallAsDisconnected(mCall, + new DisconnectCause(DisconnectCause.ERROR, + "did not hold in timeout window")); + mCallsManager.markCallAsRemoved(mCall); + } + } + + @VisibleForTesting + public CompletableFuture<Integer> getCallStateOrTimeoutResult() { + return mCallStateOrTimeoutResult; + } + + @VisibleForTesting + public CompletableFuture<VoipCallTransactionResult> getTransactionResult() { + return mTransactionResult; + } + + @VisibleForTesting + public Call.CallStateListener getCallStateListenerImpl() { + return mCallStateListenerImpl; + } +} diff --git a/testapps/transactionalVoipApp/res/values-am/strings.xml b/testapps/transactionalVoipApp/res/values-am/strings.xml index 120a9b9ce..d71c28739 100644 --- a/testapps/transactionalVoipApp/res/values-am/strings.xml +++ b/testapps/transactionalVoipApp/res/values-am/strings.xml @@ -29,7 +29,7 @@ <string name="set_call_inactive" msgid="7106775211368705195">"ወደ ገቢር ያልሆነ ተቀናብሯል"</string> <string name="disconnect_call" msgid="1349412380315371385">"ግንኙነትን ያቋርጡ"</string> <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ማዳመጫ"</string> - <string name="request_speaker_endpoint" msgid="1033259535289845405">"ድምጽ ማውጫ"</string> + <string name="request_speaker_endpoint" msgid="1033259535289845405">"ድምፅ ማውጫ"</string> <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"ብሉቱዝ"</string> <string name="start_stream" msgid="3567634786280097431">"ዥረት ይጀምሩ"</string> <string name="crash_app" msgid="2548690390730057704">"ለየት ያለ ነገርን ይጣሉ"</string> diff --git a/testapps/transactionalVoipApp/res/values-ca/strings.xml b/testapps/transactionalVoipApp/res/values-ca/strings.xml index 06f165569..550044425 100644 --- a/testapps/transactionalVoipApp/res/values-ca/strings.xml +++ b/testapps/transactionalVoipApp/res/values-ca/strings.xml @@ -31,7 +31,7 @@ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Auricular"</string> <string name="request_speaker_endpoint" msgid="1033259535289845405">"Altaveu"</string> <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string> - <string name="start_stream" msgid="3567634786280097431">"inicia la reproducció en continu"</string> + <string name="start_stream" msgid="3567634786280097431">"inicia la reproducció en línia"</string> <string name="crash_app" msgid="2548690390730057704">"llança una excepció"</string> <string name="update_notification" msgid="8677916482672588779">"actualitza la notificació a l\'estil de trucada en curs"</string> </resources> diff --git a/testapps/transactionalVoipApp/res/values-kk/strings.xml b/testapps/transactionalVoipApp/res/values-kk/strings.xml index 6511211ca..03fd03129 100644 --- a/testapps/transactionalVoipApp/res/values-kk/strings.xml +++ b/testapps/transactionalVoipApp/res/values-kk/strings.xml @@ -28,7 +28,7 @@ <string name="answer" msgid="5423590397665409939">"жауап беру"</string> <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string> <string name="disconnect_call" msgid="1349412380315371385">"ажырату"</string> - <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Динамик"</string> + <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Телефон динамигі"</string> <string name="request_speaker_endpoint" msgid="1033259535289845405">"Динамик"</string> <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string> <string name="start_stream" msgid="3567634786280097431">"трансляцияны бастау"</string> diff --git a/testapps/transactionalVoipApp/res/values-zh-rCN/strings.xml b/testapps/transactionalVoipApp/res/values-zh-rCN/strings.xml index a434e3595..a74cbb539 100644 --- a/testapps/transactionalVoipApp/res/values-zh-rCN/strings.xml +++ b/testapps/transactionalVoipApp/res/values-zh-rCN/strings.xml @@ -19,7 +19,7 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="app_name" msgid="2907804426411305091">"事务性 API 测试活动"</string> <string name="in_call_activity_name" msgid="7545884666442897585">"通话活动中的事务"</string> - <string name="register_phone_account" msgid="1920315963082350332">"注册电话帐号"</string> + <string name="register_phone_account" msgid="1920315963082350332">"注册电话账号"</string> <string name="start_foreground_service" msgid="8968755699895128574">"启动 FGS(在后台模拟 MT + 应用)"</string> <string name="start_outgoing" msgid="1441644037370361864">"开始去电"</string> <string name="start_incoming" msgid="6444983300186361271">"开始来电"</string> diff --git a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java index 5da7f31a3..54aedc4f1 100644 --- a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java +++ b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java @@ -381,7 +381,7 @@ public class AnalyticsTests extends TelecomSystemTest { waitForHandlerAction( mTelecomSystem.getCallsManager().getCallAudioManager() - .getCallAudioRouteStateMachine().getHandler(), + .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT); waitForHandlerAction( mTelecomSystem.getCallsManager().getCallAudioManager() @@ -391,7 +391,7 @@ public class AnalyticsTests extends TelecomSystemTest { mInCallServiceFixtureX.getInCallAdapter().setAudioRoute(CallAudioState.ROUTE_SPEAKER, null); waitForHandlerAction( mTelecomSystem.getCallsManager().getCallAudioManager() - .getCallAudioRouteStateMachine().getHandler(), + .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT); waitForHandlerAction( mTelecomSystem.getCallsManager().getCallAudioManager() diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java index 68eb8b262..d2937e234 100644 --- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java +++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java @@ -47,11 +47,8 @@ import android.media.AudioManager; import android.net.Uri; import android.os.Binder; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.os.Process; import android.os.UserHandle; -import android.os.UserManager; import android.provider.BlockedNumberContract; import android.telecom.Call; import android.telecom.CallAudioState; @@ -72,6 +69,8 @@ import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; import com.android.internal.telecom.IInCallAdapter; +import com.android.server.telecom.InCallController; + import android.telecom.CallerInfo; import com.google.common.base.Predicate; @@ -110,6 +109,7 @@ public class BasicCallTests extends TelecomSystemTest { doReturn(mContext).when(mContext).createContextAsUser(any(UserHandle.class), anyInt()); mPackageManager = mContext.getPackageManager(); when(mPackageManager.getPackageUid(anyString(), eq(0))).thenReturn(Binder.getCallingUid()); + when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false); } @Override @@ -626,6 +626,48 @@ public class BasicCallTests extends TelecomSystemTest { @LargeTest @Test + public void testIncomingThenOutgoingCalls_AssociatedUsersNotEqual() throws Exception { + when(mFeatureFlags.workProfileAssociatedUser()).thenReturn(true); + InCallServiceFixture.setIgnoreOverrideAdapterFlag(true); + + // Receive incoming call via mPhoneAccountMultiUser + IdPair incoming = startAndMakeActiveIncomingCall("650-555-2323", + mPhoneAccountMultiUser.getAccountHandle(), mConnectionServiceFixtureA); + waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(), + TEST_TIMEOUT); + // Make outgoing call on mPhoneAccountMultiUser (unassociated sim to simulate guest/ + // secondary user scenario where both MO/MT calls exist). + IdPair outgoing = startAndMakeActiveOutgoingCall("650-555-1212", + mPhoneAccountMultiUser.getAccountHandle(), mConnectionServiceFixtureA); + waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(), + TEST_TIMEOUT); + + // Outgoing call should be on hold while incoming call is made active + mConnectionServiceFixtureA.mConnectionById.get(incoming.mConnectionId).state = + Connection.STATE_HOLDING; + + // Swap calls and verify that outgoing call is now the active call while the incoming call + // is the held call. + mConnectionServiceFixtureA.sendSetOnHold(outgoing.mConnectionId); + waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(), + TEST_TIMEOUT); + assertEquals(Call.STATE_HOLDING, + mInCallServiceFixtureX.getCall(outgoing.mCallId).getState()); + assertEquals(Call.STATE_ACTIVE, + mInCallServiceFixtureX.getCall(incoming.mCallId).getState()); + + // Ensure no issues with call disconnect. + mInCallServiceFixtureX.mInCallAdapter.disconnectCall(incoming.mCallId); + mInCallServiceFixtureX.mInCallAdapter.disconnectCall(outgoing.mCallId); + assertEquals(Call.STATE_DISCONNECTING, + mInCallServiceFixtureX.getCall(incoming.mCallId).getState()); + assertEquals(Call.STATE_DISCONNECTING, + mInCallServiceFixtureX.getCall(outgoing.mCallId).getState()); + InCallServiceFixture.setIgnoreOverrideAdapterFlag(false); + } + + @LargeTest + @Test public void testAudioManagerOperations() throws Exception { AudioManager audioManager = (AudioManager) mComponentContextFixture.getTestDouble() .getApplicationContext().getSystemService(Context.AUDIO_SERVICE); @@ -648,7 +690,7 @@ public class BasicCallTests extends TelecomSystemTest { mInCallServiceFixtureX.mInCallAdapter.setAudioRoute(CallAudioState.ROUTE_SPEAKER, null); waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager() - .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT); + .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT); ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor = ArgumentCaptor.forClass(AudioDeviceInfo.class); verify(audioManager, timeout(TEST_TIMEOUT).atLeast(1)) @@ -656,7 +698,7 @@ public class BasicCallTests extends TelecomSystemTest { assertEquals(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, infoArgumentCaptor.getValue().getType()); mInCallServiceFixtureX.mInCallAdapter.setAudioRoute(CallAudioState.ROUTE_EARPIECE, null); waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager() - .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT); + .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT); // setSpeakerPhoneOn(false) gets called once during the call initiation phase verify(audioManager, timeout(TEST_TIMEOUT).atLeast(1)) .clearCommunicationDevice(); @@ -667,7 +709,7 @@ public class BasicCallTests extends TelecomSystemTest { waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager() .getCallAudioModeStateMachine().getHandler(), TEST_TIMEOUT); waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager() - .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT); + .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT); verify(audioManager, timeout(TEST_TIMEOUT)) .abandonAudioFocusForCall(); verify(audioManager, timeout(TEST_TIMEOUT).atLeastOnce()) @@ -995,6 +1037,7 @@ public class BasicCallTests extends TelecomSystemTest { call.setTargetPhoneAccount(mPhoneAccountA1.getAccountHandle()); assert(call.isVideoCallingSupportedByPhoneAccount()); assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState()); + call.setIsCreateConnectionComplete(true); } /** @@ -1018,6 +1061,7 @@ public class BasicCallTests extends TelecomSystemTest { call.setTargetPhoneAccount(mPhoneAccountA2.getAccountHandle()); assert(!call.isVideoCallingSupportedByPhoneAccount()); assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState()); + call.setIsCreateConnectionComplete(true); } /** @@ -1195,7 +1239,7 @@ public class BasicCallTests extends TelecomSystemTest { .getState()); mInCallServiceFixtureX.mInCallAdapter.mute(true); waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager() - .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT); + .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT); assertTrue(mTelecomSystem.getCallsManager().getAudioState().isMuted()); // Make an emergency call. @@ -1204,14 +1248,14 @@ public class BasicCallTests extends TelecomSystemTest { assertEquals(Call.STATE_DIALING, mInCallServiceFixtureX.getCall(emergencyCall.mCallId) .getState()); waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager() - .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT); + .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT); // Should be unmute automatically. assertFalse(mTelecomSystem.getCallsManager().getAudioState().isMuted()); // Toggle mute during an emergency call. mTelecomSystem.getCallsManager().getCallAudioManager().toggleMute(); waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager() - .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT); + .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT); // Should keep unmute. assertFalse(mTelecomSystem.getCallsManager().getAudioState().isMuted()); diff --git a/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java b/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java index 7df4f292a..a98c1eea0 100644 --- a/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java +++ b/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java @@ -72,6 +72,7 @@ public class BlockCheckerFilterTest extends TelecomTestCase { private static final CallFilteringResult BLOCK_RESULT = new CallFilteringResult.Builder() .setShouldAllowCall(false) .setShouldReject(true) + .setShouldSilence(true) .setShouldAddToCallLog(true) .setShouldShowNotification(false) .setCallBlockReason(CallLog.Calls.BLOCK_REASON_BLOCKED_NUMBER) diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java index a12846845..977cef9b8 100644 --- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java +++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java @@ -52,6 +52,7 @@ import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -107,7 +108,7 @@ public class BluetoothDeviceManagerTest extends TelecomTestCase { mContext = mComponentContextFixture.getTestDouble().getApplicationContext(); mCommunicationDeviceTracker = new CallAudioCommunicationDeviceTracker(mContext); mBluetoothDeviceManager = new BluetoothDeviceManager(mContext, mAdapter, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, mFeatureFlags); mBluetoothDeviceManager.setBluetoothRouteManager(mRouteManager); mCommunicationDeviceTracker.setBluetoothRouteManager(mRouteManager); @@ -120,7 +121,7 @@ public class BluetoothDeviceManagerTest extends TelecomTestCase { serviceListenerUnderTest = serviceCaptor.getValue(); receiverUnderTest = new BluetoothStateReceiver(mBluetoothDeviceManager, - mRouteManager, mCommunicationDeviceTracker); + mRouteManager, mCommunicationDeviceTracker, mFeatureFlags); mBluetoothDeviceManager.setHeadsetServiceForTesting(mBluetoothHeadset); mBluetoothDeviceManager.setHearingAidServiceForTesting(mBluetoothHearingAid); @@ -131,6 +132,7 @@ public class BluetoothDeviceManagerTest extends TelecomTestCase { verify(mBluetoothLeAudio).registerCallback(any(), leAudioCallbacksTest.capture()); when(mSpeakerInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER); + when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(false); } @Override @@ -423,6 +425,7 @@ public class BluetoothDeviceManagerTest extends TelecomTestCase { List<AudioDeviceInfo> devices = new ArrayList<>(); devices.add(mockAudioDeviceInfo); + when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo); when(mockAudioManager.getAvailableCommunicationDevices()) .thenReturn(devices); when(mockAudioManager.setCommunicationDevice(eq(mockAudioDeviceInfo))) @@ -439,7 +442,7 @@ public class BluetoothDeviceManagerTest extends TelecomTestCase { when(mockAudioManager.getCommunicationDevice()).thenReturn(mockAudioDeviceInfo); mBluetoothDeviceManager.disconnectAudio(); - verify(mockAudioManager).clearCommunicationDevice(); + verify(mockAudioManager, atLeastOnce()).clearCommunicationDevice(); } @SmallTest @@ -458,6 +461,7 @@ public class BluetoothDeviceManagerTest extends TelecomTestCase { List<AudioDeviceInfo> devices = new ArrayList<>(); devices.add(mockAudioDeviceInfo); + when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo); when(mockAudioManager.getAvailableCommunicationDevices()) .thenReturn(devices); when(mockAudioManager.setCommunicationDevice(mockAudioDeviceInfo)) @@ -475,7 +479,7 @@ public class BluetoothDeviceManagerTest extends TelecomTestCase { BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO)); mBluetoothDeviceManager.disconnectAudio(); - verify(mockAudioManager).clearCommunicationDevice(); + verify(mockAudioManager, atLeastOnce()).clearCommunicationDevice(); } @SmallTest @@ -514,6 +518,7 @@ public class BluetoothDeviceManagerTest extends TelecomTestCase { @SmallTest @Test public void testConnectMultipleLeAudioDevices() { + when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true); receiverUnderTest.setIsInCall(true); receiverUnderTest.onReceive(mContext, buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1, @@ -561,13 +566,8 @@ public class BluetoothDeviceManagerTest extends TelecomTestCase { @SmallTest @Test public void testClearCommunicationDeviceOnActiveDeviceChange() { + when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true); receiverUnderTest.setIsInCall(true); -// receiverUnderTest.onReceive(mContext, -// buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1, -// BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO)); -// leAudioCallbacksTest.getValue().onGroupNodeAdded(device1, 1); -// when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class), -// eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true); List<AudioDeviceInfo> devices = new ArrayList<>(); AudioDeviceInfo leAudioDevice1 = createMockAudioDeviceInfo(device1.getAddress(), @@ -596,6 +596,7 @@ public class BluetoothDeviceManagerTest extends TelecomTestCase { @SmallTest @Test public void testConnectDualModeEarbud() { + when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true); receiverUnderTest.setIsInCall(true); // LE Audio earbuds connected @@ -689,46 +690,28 @@ public class BluetoothDeviceManagerTest extends TelecomTestCase { @SmallTest @Test - public void testClearHearingAidCommunicationDevice() { - AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class); - when(mockAudioDeviceInfo.getAddress()).thenReturn(DEVICE_ADDRESS_1); - when(mockAudioDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_HEARING_AID); - List<AudioDeviceInfo> devices = new ArrayList<>(); - devices.add(mockAudioDeviceInfo); - - when(mockAudioManager.getAvailableCommunicationDevices()) - .thenReturn(devices); - when(mockAudioManager.setCommunicationDevice(eq(mockAudioDeviceInfo))) - .thenReturn(true); - - mCommunicationDeviceTracker.setCommunicationDevice(AudioDeviceInfo.TYPE_HEARING_AID, null); - when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo); - mCommunicationDeviceTracker.clearCommunicationDevice(AudioDeviceInfo.TYPE_HEARING_AID); - verify(mRouteManager).onAudioLost(eq(DEVICE_ADDRESS_1)); - assertFalse(mCommunicationDeviceTracker.isAudioDeviceSetForType( - AudioDeviceInfo.TYPE_HEARING_AID)); + public void testClearHearingAidCommunicationDeviceLegacy() { + assertClearHearingAidOrLeCommunicationDevice(false, AudioDeviceInfo.TYPE_HEARING_AID); } @SmallTest @Test - public void testClearLeAudioCommunicationDevice() { - AudioDeviceInfo mockAudioDeviceInfo = createMockAudioDeviceInfo(DEVICE_ADDRESS_1, - AudioDeviceInfo.TYPE_BLE_HEADSET); - List<AudioDeviceInfo> devices = new ArrayList<>(); - devices.add(mockAudioDeviceInfo); + public void testClearHearingAidCommunicationDeviceWithFlag() { + when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true); + assertClearHearingAidOrLeCommunicationDevice(true, AudioDeviceInfo.TYPE_HEARING_AID); + } - when(mockAudioManager.getAvailableCommunicationDevices()) - .thenReturn(devices); - when(mockAudioManager.setCommunicationDevice(eq(mockAudioDeviceInfo))) - .thenReturn(true); + @SmallTest + @Test + public void testClearLeAudioCommunicationDeviceLegacy() { + assertClearHearingAidOrLeCommunicationDevice(false, AudioDeviceInfo.TYPE_BLE_HEADSET); + } - mCommunicationDeviceTracker.setCommunicationDevice( - AudioDeviceInfo.TYPE_BLE_HEADSET, device1); - when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo); - mCommunicationDeviceTracker.clearCommunicationDevice(AudioDeviceInfo.TYPE_BLE_HEADSET); - verify(mRouteManager).onAudioLost(eq(DEVICE_ADDRESS_1)); - assertFalse(mCommunicationDeviceTracker.isAudioDeviceSetForType( - AudioDeviceInfo.TYPE_BLE_HEADSET)); + @SmallTest + @Test + public void testClearLeAudioCommunicationDeviceWithFlag() { + when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true); + assertClearHearingAidOrLeCommunicationDevice(true, AudioDeviceInfo.TYPE_BLE_HEADSET); } @SmallTest @@ -772,6 +755,47 @@ public class BluetoothDeviceManagerTest extends TelecomTestCase { assertTrue(mBluetoothDeviceManager.isInbandRingingEnabled()); } + private void assertClearHearingAidOrLeCommunicationDevice( + boolean flagEnabled, int device_type + ) { + AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class); + when(mockAudioDeviceInfo.getAddress()).thenReturn(DEVICE_ADDRESS_1); + when(mockAudioDeviceInfo.getType()).thenReturn(device_type); + List<AudioDeviceInfo> devices = new ArrayList<>(); + devices.add(mockAudioDeviceInfo); + + when(mockAudioManager.getAvailableCommunicationDevices()) + .thenReturn(devices); + when(mockAudioManager.setCommunicationDevice(eq(mockAudioDeviceInfo))) + .thenReturn(true); + + if (flagEnabled) { + BluetoothDevice btDevice = device_type == AudioDeviceInfo.TYPE_BLE_HEADSET + ? device1 : null; + mCommunicationDeviceTracker.setCommunicationDevice(device_type, btDevice); + } else { + if (device_type == AudioDeviceInfo.TYPE_BLE_HEADSET) { + mBluetoothDeviceManager.setLeAudioCommunicationDevice(); + } else { + mBluetoothDeviceManager.setHearingAidCommunicationDevice(); + } + } + when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo); + if (flagEnabled) { + mCommunicationDeviceTracker.clearCommunicationDevice(device_type); + assertFalse(mCommunicationDeviceTracker.isAudioDeviceSetForType(device_type)); + } else { + if (device_type == AudioDeviceInfo.TYPE_BLE_HEADSET) { + mBluetoothDeviceManager.clearLeAudioOrSpeakerCommunicationDevice(); + assertFalse(mBluetoothDeviceManager.isLeAudioCommunicationDevice()); + } else { + mBluetoothDeviceManager.clearHearingAidOrSpeakerCommunicationDevice(); + assertFalse(mBluetoothDeviceManager.isHearingAidSetAsCommunicationDevice()); + } + } + verify(mRouteManager).onAudioLost(eq(DEVICE_ADDRESS_1)); + } + private AudioDeviceInfo createMockAudioDeviceInfo(String address, int audioType) { AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class); when(mockAudioDeviceInfo.getType()).thenReturn(audioType); diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java index 2b5e5ac59..e1ef08ae2 100644 --- a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java +++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java @@ -16,6 +16,16 @@ package com.android.server.telecom.tests; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; @@ -28,6 +38,8 @@ import android.os.Parcel; import android.telecom.Log; import android.test.suitebuilder.annotation.SmallTest; +import android.media.AudioDeviceInfo; + import com.android.internal.os.SomeArgs; import com.android.server.telecom.CallAudioCommunicationDeviceTracker; import com.android.server.telecom.TelecomSystem; @@ -47,23 +59,20 @@ import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @RunWith(JUnit4.class) public class BluetoothRouteManagerTest extends TelecomTestCase { private static final int TEST_TIMEOUT = 1000; static final BluetoothDevice DEVICE1 = makeBluetoothDevice("00:00:00:00:00:01"); static final BluetoothDevice DEVICE2 = makeBluetoothDevice("00:00:00:00:00:02"); static final BluetoothDevice DEVICE3 = makeBluetoothDevice("00:00:00:00:00:03"); - static final BluetoothDevice HEARING_AID_DEVICE = makeBluetoothDevice("00:00:00:00:00:04"); + static final BluetoothDevice HEARING_AID_DEVICE_LEFT = makeBluetoothDevice("CA:FE:DE:CA:00:01"); + static final BluetoothDevice HEARING_AID_DEVICE_RIGHT = + makeBluetoothDevice("CA:FE:DE:CA:00:02"); + // See HearingAidService#getActiveDevices + // Note: It is really important that the left HA is the first one. The left HA is always + // in the first index (0) and the right one in the second index (1). + static final BluetoothDevice[] HEARING_AIDS = + new BluetoothDevice[]{HEARING_AID_DEVICE_LEFT, HEARING_AID_DEVICE_RIGHT}; @Mock private BluetoothAdapter mBluetoothAdapter; @Mock private BluetoothDeviceManager mDeviceManager; @@ -88,6 +97,59 @@ public class BluetoothRouteManagerTest extends TelecomTestCase { @SmallTest @Test + public void testConnectLeftHearingAidWhenLeftIsActive() { + BluetoothRouteManager sm = setupStateMachine( + BluetoothRouteManager.AUDIO_OFF_STATE_NAME, HEARING_AID_DEVICE_LEFT); + sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT, + BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID); + when(mDeviceManager.connectAudio(anyString(), anyBoolean())).thenReturn(true); + when(mDeviceManager.isHearingAidSetAsCommunicationDevice()).thenReturn(true); + + setupConnectedDevices(null, HEARING_AIDS, null, null, HEARING_AIDS, null); + when(mBluetoothHeadset.getAudioState(nullable(BluetoothDevice.class))) + .thenReturn(BluetoothHeadset.STATE_AUDIO_DISCONNECTED); + + executeRoutingAction(sm, + BluetoothRouteManager.NEW_DEVICE_CONNECTED, HEARING_AID_DEVICE_LEFT.getAddress()); + + executeRoutingAction(sm, + BluetoothRouteManager.CONNECT_BT, HEARING_AID_DEVICE_LEFT.getAddress()); + + assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX + + ":" + HEARING_AID_DEVICE_LEFT.getAddress(), sm.getCurrentState().getName()); + + sm.quitNow(); + } + + @SmallTest + @Test + public void testConnectRightHearingAidWhenLeftIsActive() { + BluetoothRouteManager sm = setupStateMachine( + BluetoothRouteManager.AUDIO_OFF_STATE_NAME, HEARING_AID_DEVICE_RIGHT); + sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT, + BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID); + when(mDeviceManager.connectAudio(anyString(), anyBoolean())).thenReturn(true); + when(mDeviceManager.isHearingAidSetAsCommunicationDevice()).thenReturn(true); + + + setupConnectedDevices(null, HEARING_AIDS, null, null, HEARING_AIDS, null); + when(mBluetoothHeadset.getAudioState(nullable(BluetoothDevice.class))) + .thenReturn(BluetoothHeadset.STATE_AUDIO_DISCONNECTED); + + executeRoutingAction(sm, + BluetoothRouteManager.NEW_DEVICE_CONNECTED, HEARING_AID_DEVICE_LEFT.getAddress()); + + executeRoutingAction(sm, + BluetoothRouteManager.CONNECT_BT, HEARING_AID_DEVICE_LEFT.getAddress()); + + assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX + + ":" + HEARING_AID_DEVICE_LEFT.getAddress(), sm.getCurrentState().getName()); + + sm.quitNow(); + } + + @SmallTest + @Test public void testConnectBtRetryWhileNotConnected() { BluetoothRouteManager sm = setupStateMachine( BluetoothRouteManager.AUDIO_OFF_STATE_NAME, null); @@ -114,15 +176,15 @@ public class BluetoothRouteManagerTest extends TelecomTestCase { BluetoothRouteManager sm = setupStateMachine( BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1); setupConnectedDevices(new BluetoothDevice[]{DEVICE1}, - new BluetoothDevice[]{HEARING_AID_DEVICE}, new BluetoothDevice[]{DEVICE2}, - DEVICE1, HEARING_AID_DEVICE, DEVICE2); + HEARING_AIDS, new BluetoothDevice[]{DEVICE2}, + DEVICE1, HEARING_AIDS, DEVICE2); sm.onActiveDeviceChanged(DEVICE1, BluetoothDeviceManager.DEVICE_TYPE_HEADSET); sm.onActiveDeviceChanged(DEVICE2, BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO); - sm.onActiveDeviceChanged(HEARING_AID_DEVICE, + sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT, BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID); executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST, DEVICE1.getAddress()); - verifyConnectionAttempt(HEARING_AID_DEVICE, 0); + verifyConnectionAttempt(HEARING_AID_DEVICE_LEFT, 0); verifyConnectionAttempt(DEVICE1, 0); verifyConnectionAttempt(DEVICE2, 0); assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX @@ -173,12 +235,52 @@ public class BluetoothRouteManagerTest extends TelecomTestCase { sm.quitNow(); } + @SmallTest + @Test + public void testSkipInactiveBtDeviceWhenEvaluateActualState() { + BluetoothRouteManager sm = setupStateMachine( + BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, HEARING_AID_DEVICE_LEFT); + setupConnectedDevices(null, HEARING_AIDS, + null, null, HEARING_AIDS, null); + executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST, + HEARING_AID_DEVICE_LEFT.getAddress()); + assertEquals(BluetoothRouteManager.AUDIO_OFF_STATE_NAME, sm.getCurrentState().getName()); + sm.quitNow(); + } + + @SmallTest + @Test + public void testConnectBtWithoutAddress() { + when(mFeatureFlags.useActualAddressToEnterConnectingState()).thenReturn(true); + BluetoothRouteManager sm = setupStateMachine( + BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1); + setupConnectedDevices(new BluetoothDevice[]{DEVICE1, DEVICE2}, null, null, null, null, + null); + when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis( + nullable(ContentResolver.class))).thenReturn(0L); + when(mBluetoothHeadset.connectAudio()).thenReturn(BluetoothStatusCodes.ERROR_UNKNOWN); + executeRoutingAction(sm, BluetoothRouteManager.CONNECT_BT, null); + // Wait 3 times: the first connection attempt is accounted for in executeRoutingAction, + // so wait twice for the retry attempt, again to make sure there are only three attempts, + // and once more for good luck. + waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); + verifyConnectionAttempt(DEVICE1, 1); + assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX + + ":" + DEVICE1.getAddress(), + sm.getCurrentState().getName()); + sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT); + sm.quitNow(); + } + private BluetoothRouteManager setupStateMachine(String initialState, BluetoothDevice initialDevice) { resetMocks(); BluetoothRouteManager sm = new BluetoothRouteManager(mContext, new TelecomSystem.SyncRoot() { }, mDeviceManager, - mTimeoutsAdapter, mCommunicationDeviceTracker); + mTimeoutsAdapter, mCommunicationDeviceTracker, mFeatureFlags); sm.setListener(mListener); sm.setInitialStateForTesting(initialState, initialDevice); waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); @@ -188,10 +290,11 @@ public class BluetoothRouteManagerTest extends TelecomTestCase { private void setupConnectedDevices(BluetoothDevice[] hfpDevices, BluetoothDevice[] hearingAidDevices, BluetoothDevice[] leAudioDevices, - BluetoothDevice hfpActiveDevice, BluetoothDevice hearingAidActiveDevice, + BluetoothDevice hfpActiveDevice, BluetoothDevice[] hearingAidActiveDevices, BluetoothDevice leAudioDevice) { if (hfpDevices == null) hfpDevices = new BluetoothDevice[]{}; if (hearingAidDevices == null) hearingAidDevices = new BluetoothDevice[]{}; + if (hearingAidActiveDevices == null) hearingAidActiveDevices = new BluetoothDevice[]{}; if (leAudioDevice == null) leAudioDevices = new BluetoothDevice[]{}; when(mDeviceManager.getNumConnectedDevices()).thenReturn( @@ -210,7 +313,7 @@ public class BluetoothRouteManagerTest extends TelecomTestCase { when(mBluetoothHearingAid.getConnectedDevices()) .thenReturn(Arrays.asList(hearingAidDevices)); when(mBluetoothAdapter.getActiveDevices(eq(BluetoothProfile.HEARING_AID))) - .thenReturn(Arrays.asList(hearingAidActiveDevice, null)); + .thenReturn(Arrays.asList(hearingAidActiveDevices)); when(mBluetoothAdapter.getActiveDevices(eq(BluetoothProfile.LE_AUDIO))) .thenReturn(Arrays.asList(leAudioDevice, null)); } diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java index 15a81d4c0..65854af35 100644 --- a/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java +++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java @@ -419,7 +419,7 @@ public class BluetoothRouteTransitionTests extends TelecomTestCase { nullable(ContentResolver.class))).thenReturn(100000L); BluetoothRouteManager sm = new BluetoothRouteManager(mContext, new TelecomSystem.SyncRoot() { }, mDeviceManager, - mTimeoutsAdapter, mCommunicationDeviceTracker); + mTimeoutsAdapter, mCommunicationDeviceTracker, mFeatureFlags); sm.setListener(mListener); sm.setInitialStateForTesting(initialState, initialDevice); waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); diff --git a/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java b/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java index 7e197fe65..86d24f96f 100644 --- a/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java +++ b/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.when; import android.content.ComponentName; import android.net.Uri; +import android.os.UserHandle; import android.telecom.DisconnectCause; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; @@ -123,6 +124,7 @@ public class CallAnomalyWatchdogTest extends TelecomTestCase { mCallAnomalyWatchdog = new CallAnomalyWatchdog(mTestScheduledExecutorService, mLock, mTimeouts, mMockClockProxy, mMockEmergencyCallDiagnosticLogger); mCallAnomalyWatchdog.setAnomalyReporterAdapter(mAnomalyReporterAdapter); + when(mMockCallsManager.getCurrentUserHandle()).thenReturn(UserHandle.CURRENT); } @Override @@ -862,6 +864,7 @@ public class CallAnomalyWatchdogTest extends TelecomTestCase { false /* shouldAttachToExistingConnection*/, false /* isConference */, mMockClockProxy, - mMockToastProxy); + mMockToastProxy, + mFeatureFlags); } }
\ No newline at end of file diff --git a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java index c8ceea95c..df281ca49 100644 --- a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java +++ b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java @@ -36,6 +36,7 @@ import com.android.server.telecom.RingbackPlayer; import com.android.server.telecom.Ringer; import com.android.server.telecom.TelecomSystem; import com.android.server.telecom.bluetooth.BluetoothStateReceiver; +import com.android.server.telecom.flags.FeatureFlags; import org.junit.After; import org.junit.Before; @@ -79,6 +80,8 @@ public class CallAudioManagerTest extends TelecomTestCase { @Mock private BluetoothStateReceiver mBluetoothStateReceiver; @Mock private TelecomSystem.SyncRoot mLock; + @Mock private FeatureFlags mFlags; + private CallAudioManager mCallAudioManager; @Override @@ -94,6 +97,7 @@ public class CallAudioManagerTest extends TelecomTestCase { return mockInCallTonePlayer; }).when(mPlayerFactory).createPlayer(anyInt()); when(mCallsManager.getLock()).thenReturn(mLock); + when(mFlags.ensureAudioModeUpdatesOnForegroundCallChange()).thenReturn(true); mCallAudioManager = new CallAudioManager( mCallAudioRouteStateMachine, mCallsManager, @@ -102,7 +106,8 @@ public class CallAudioManagerTest extends TelecomTestCase { mRinger, mRingbackPlayer, mBluetoothStateReceiver, - mDtmfLocalTonePlayer); + mDtmfLocalTonePlayer, + mFlags); } @Override @@ -278,19 +283,27 @@ public class CallAudioManagerTest extends TelecomTestCase { verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs( eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture()); assertMessageArgEquality(expectedArgs, captor.getValue()); - // Expet another invocation due to audio mode change signal. - verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs( - anyInt(), any(CallAudioModeStateMachine.MessageArgs.class)); - + if (mFlags.ensureAudioModeUpdatesOnForegroundCallChange()) { + // Expect another invocation due to audio mode change signal. + verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs( + anyInt(), any(CallAudioModeStateMachine.MessageArgs.class)); + } else { + verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs( + anyInt(), any(CallAudioModeStateMachine.MessageArgs.class)); + } when(call.getState()).thenReturn(CallState.ACTIVE); mCallAudioManager.onCallStateChanged(call, CallState.DIALING, CallState.ACTIVE); verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs( eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture()); assertMessageArgEquality(expectedArgs, captor.getValue()); - verify(mCallAudioModeStateMachine, times(4)).sendMessageWithArgs( - anyInt(), any(CallAudioModeStateMachine.MessageArgs.class)); - + if (mFlags.ensureAudioModeUpdatesOnForegroundCallChange()) { + verify(mCallAudioModeStateMachine, times(4)).sendMessageWithArgs( + anyInt(), any(CallAudioModeStateMachine.MessageArgs.class)); + } else { + verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs( + anyInt(), any(CallAudioModeStateMachine.MessageArgs.class)); + } disconnectCall(call); stopTone(); @@ -298,6 +311,12 @@ public class CallAudioManagerTest extends TelecomTestCase { verifyProperCleanup(); } + @Test + public void testSingleOutgoingCallWithoutAudioModeUpdateOnForegroundCallChange() { + when(mFlags.ensureAudioModeUpdatesOnForegroundCallChange()).thenReturn(false); + testSingleOutgoingCall(); + } + @MediumTest @Test public void testRingbackStartStop() { @@ -329,9 +348,14 @@ public class CallAudioManagerTest extends TelecomTestCase { verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs( eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture()); assertMessageArgEquality(expectedArgs, captor.getValue()); - // Expect an extra time due to audio mode change signal - verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs( - anyInt(), any(CallAudioModeStateMachine.MessageArgs.class)); + if (mFlags.ensureAudioModeUpdatesOnForegroundCallChange()) { + // Expect an extra time due to audio mode change signal + verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs( + anyInt(), any(CallAudioModeStateMachine.MessageArgs.class)); + } else { + verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs( + anyInt(), any(CallAudioModeStateMachine.MessageArgs.class)); + } // Ensure we started ringback. verify(mRingbackPlayer).startRingbackForCall(any(Call.class)); @@ -353,6 +377,12 @@ public class CallAudioManagerTest extends TelecomTestCase { verify(mRingbackPlayer, times(1)).startRingbackForCall(any(Call.class)); } + @Test + public void testRingbackStartStopWithoutAudioModeUpdateOnForegroundCallChange() { + when(mFlags.ensureAudioModeUpdatesOnForegroundCallChange()).thenReturn(false); + testRingbackStartStop(); + } + @SmallTest @Test public void testNewCallGoesToAudioProcessing() { @@ -708,6 +738,10 @@ public class CallAudioManagerTest extends TelecomTestCase { @SmallTest @Test public void testTriggerAudioManagerModeChange() { + if (!mFlags.ensureAudioModeUpdatesOnForegroundCallChange()) { + // Skip if the new behavior isn't in use. + return; + } // Start with an incoming PSTN call Call pstnCall = mock(Call.class); when(pstnCall.getState()).thenReturn(CallState.RINGING); diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java index d7854a5a9..cddf2ad85 100644 --- a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java +++ b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java @@ -16,14 +16,30 @@ package com.android.server.telecom.tests; +import static com.android.server.telecom.CallAudioModeStateMachine.CALL_AUDIO_FOCUS_REQUEST; +import static com.android.server.telecom.CallAudioModeStateMachine.RING_AUDIO_FOCUS_REQUEST; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.media.AudioFocusRequest; import android.media.AudioManager; import android.os.HandlerThread; import android.test.suitebuilder.annotation.SmallTest; +import com.android.server.telecom.CallAudioCommunicationDeviceTracker; import com.android.server.telecom.CallAudioManager; import com.android.server.telecom.CallAudioModeStateMachine; -import com.android.server.telecom.CallAudioRouteStateMachine; import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs.Builder; +import com.android.server.telecom.CallAudioRouteStateMachine; import com.android.server.telecom.SystemStateHelper; import org.junit.After; @@ -31,18 +47,9 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @RunWith(JUnit4.class) public class CallAudioModeStateMachineTest extends TelecomTestCase { private static final int TEST_TIMEOUT = 1000; @@ -51,6 +58,7 @@ public class CallAudioModeStateMachineTest extends TelecomTestCase { @Mock private AudioManager mAudioManager; @Mock private CallAudioManager mCallAudioManager; @Mock private CallAudioRouteStateMachine mCallAudioRouteStateMachine; + @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker; private HandlerThread mTestThread; @@ -60,8 +68,9 @@ public class CallAudioModeStateMachineTest extends TelecomTestCase { mTestThread = new HandlerThread("CallAudioModeStateMachineTest"); mTestThread.start(); super.setUp(); - when(mCallAudioManager.getCallAudioRouteStateMachine()) + when(mCallAudioManager.getCallAudioRouteAdapter()) .thenReturn(mCallAudioRouteStateMachine); + when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false); } @Override @@ -76,7 +85,7 @@ public class CallAudioModeStateMachineTest extends TelecomTestCase { @Test public void testNoFocusWhenRingerSilenced() throws Throwable { CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper, - mAudioManager, mTestThread.getLooper()); + mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker); sm.setCallAudioManager(mCallAudioManager); sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING); waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); @@ -108,7 +117,7 @@ public class CallAudioModeStateMachineTest extends TelecomTestCase { @Test public void testSwitchToStreamingMode() { CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper, - mAudioManager, mTestThread.getLooper()); + mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker); sm.setCallAudioManager(mCallAudioManager); sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING); waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); @@ -138,7 +147,7 @@ public class CallAudioModeStateMachineTest extends TelecomTestCase { @Test public void testExitStreamingMode() { CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper, - mAudioManager, mTestThread.getLooper()); + mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker); sm.setCallAudioManager(mCallAudioManager); sm.sendMessage(CallAudioModeStateMachine.ENTER_STREAMING_FOCUS_FOR_TESTING); waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); @@ -166,7 +175,7 @@ public class CallAudioModeStateMachineTest extends TelecomTestCase { @Test public void testNoRingWhenDeviceIsAtEar() { CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper, - mAudioManager, mTestThread.getLooper()); + mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker); sm.setCallAudioManager(mCallAudioManager); sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING); sm.sendMessage(CallAudioModeStateMachine.NEW_HOLDING_CALL, new Builder() @@ -202,7 +211,7 @@ public class CallAudioModeStateMachineTest extends TelecomTestCase { @Test public void testRegainFocusWhenHfpIsConnectedSilenced() throws Throwable { CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper, - mAudioManager, mTestThread.getLooper()); + mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker); sm.setCallAudioManager(mCallAudioManager); sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING); waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); @@ -246,7 +255,7 @@ public class CallAudioModeStateMachineTest extends TelecomTestCase { @Test public void testDoNotRingTwiceWhenHfpConnected() { CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper, - mAudioManager, mTestThread.getLooper()); + mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker); sm.setCallAudioManager(mCallAudioManager); sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING); waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); @@ -284,7 +293,7 @@ public class CallAudioModeStateMachineTest extends TelecomTestCase { @Test public void testStartRingingAfterHfpConnectedIfNotAlreadyPlaying() { CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper, - mAudioManager, mTestThread.getLooper()); + mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker); sm.setCallAudioManager(mCallAudioManager); sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING); waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); @@ -318,7 +327,46 @@ public class CallAudioModeStateMachineTest extends TelecomTestCase { verify(mCallAudioManager, times(2)).startRinging(); } + @SmallTest + @Test + public void testAudioFocusRequestWithResolveHiddenDependencies() { + CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper, + mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker); + when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true); + ArgumentCaptor<AudioFocusRequest> captor = ArgumentCaptor.forClass(AudioFocusRequest.class); + sm.setCallAudioManager(mCallAudioManager); + + resetMocks(); + when(mCallAudioManager.startRinging()).thenReturn(true); + when(mCallAudioManager.isRingtonePlaying()).thenReturn(false); + + sm.sendMessage(CallAudioModeStateMachine.ENTER_RING_FOCUS_FOR_TESTING); + waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); + verify(mAudioManager).requestAudioFocus(captor.capture()); + assertTrue(areAudioFocusRequestsMatch(captor.getValue(), RING_AUDIO_FOCUS_REQUEST)); + + sm.sendMessage(CallAudioModeStateMachine.ENTER_CALL_FOCUS_FOR_TESTING); + waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); + verify(mAudioManager, atLeast(1)).requestAudioFocus(captor.capture()); + AudioFocusRequest request = captor.getValue(); + assertTrue(areAudioFocusRequestsMatch(request, CALL_AUDIO_FOCUS_REQUEST)); + + sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING); + } + private void resetMocks() { clearInvocations(mCallAudioManager, mAudioManager); } + + private boolean areAudioFocusRequestsMatch(AudioFocusRequest r1, AudioFocusRequest r2) { + if ((r1 == null) || (r2 == null)) { + return false; + } + + if (r1.getFocusGain() != r2.getFocusGain()) { + return false; + } + + return r1.getAudioAttributes().equals(r2.getAudioAttributes()); + } } diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java b/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java index c7e5aa9b3..3690d5f11 100644 --- a/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java +++ b/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java @@ -16,10 +16,15 @@ package com.android.server.telecom.tests; +import static com.android.server.telecom.CallAudioModeStateMachine.CALL_AUDIO_FOCUS_REQUEST; +import static com.android.server.telecom.CallAudioModeStateMachine.RING_AUDIO_FOCUS_REQUEST; + +import android.media.AudioFocusRequest; import android.media.AudioManager; import android.os.HandlerThread; import android.test.suitebuilder.annotation.SmallTest; +import com.android.server.telecom.CallAudioCommunicationDeviceTracker; import com.android.server.telecom.CallAudioManager; import com.android.server.telecom.CallAudioModeStateMachine; import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs; @@ -37,6 +42,7 @@ import java.util.Collection; import java.util.List; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; @@ -103,6 +109,7 @@ public class CallAudioModeTransitionTests extends TelecomTestCase { @Mock private SystemStateHelper mSystemStateHelper; @Mock private AudioManager mAudioManager; @Mock private CallAudioManager mCallAudioManager; + @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker; private final ModeTestParameters mParams; private HandlerThread mTestThread; @@ -130,13 +137,14 @@ public class CallAudioModeTransitionTests extends TelecomTestCase { @SmallTest public void modeTransitionTest() { CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper, - mAudioManager, mTestThread.getLooper()); + mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker); sm.setCallAudioManager(mCallAudioManager); sm.sendMessage(mParams.initialAudioState); waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT); resetMocks(); when(mCallAudioManager.startRinging()).thenReturn(true); + when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false); if (mParams.initialAudioState == CallAudioModeStateMachine.ENTER_AUDIO_PROCESSING_FOCUS_FOR_TESTING) { when(mAudioManager.getMode()) diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java index d93cb2c77..1fa14a559 100644 --- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java +++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java @@ -16,6 +16,27 @@ package com.android.server.telecom.tests; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.same; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; import android.content.Context; @@ -29,16 +50,16 @@ import android.telecom.CallAudioState; import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; -import com.android.server.telecom.CallAudioCommunicationDeviceTracker; -import com.android.server.telecom.bluetooth.BluetoothRouteManager; import com.android.server.telecom.Call; +import com.android.server.telecom.CallAudioCommunicationDeviceTracker; +import com.android.server.telecom.CallAudioManager; import com.android.server.telecom.CallAudioRouteStateMachine; import com.android.server.telecom.CallsManager; import com.android.server.telecom.ConnectionServiceWrapper; -import com.android.server.telecom.CallAudioManager; import com.android.server.telecom.StatusBarNotifier; import com.android.server.telecom.TelecomSystem; import com.android.server.telecom.WiredHeadsetManager; +import com.android.server.telecom.bluetooth.BluetoothRouteManager; import org.junit.After; import org.junit.Before; @@ -59,25 +80,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.same; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @RunWith(JUnit4.class) public class CallAudioRouteStateMachineTest extends TelecomTestCase { @@ -97,6 +99,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { @Mock Call fakeSelfManagedCall; @Mock Call fakeCall; @Mock CallAudioManager mockCallAudioManager; + @Mock BluetoothDevice mockWatchDevice; private CallAudioManager.AudioServiceFactory mAudioServiceFactory; private static final int TEST_TIMEOUT = 500; @@ -135,9 +138,12 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { when(fakeSelfManagedCall.isAlive()).thenReturn(true); when(fakeSelfManagedCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_ALL); when(fakeSelfManagedCall.isSelfManaged()).thenReturn(true); + when(mFeatureFlags.transitRouteBeforeAudioDisconnectBt()).thenReturn(false); doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class), any(CallAudioState.class)); + when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false); + when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(false); } @Override @@ -161,7 +167,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); // Since we don't know if we're on a platform with an earpiece or not, all we can do // is ensure the stateMachine construction didn't fail. But at least we exercised the @@ -182,7 +189,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); Set<Call> trackedCalls = new HashSet<>(Arrays.asList(fakeCall, fakeSelfManagedCall)); @@ -205,15 +213,121 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { stateMachine.sendMessageWithSessionInfo( CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + + // assert expected end state + assertEquals(stateMachine.getCurrentCallAudioState().getRoute(), + CallAudioRouteStateMachine.ROUTE_SPEAKER); + // should update the audio route on all tracked calls ... + verify(mockConnectionServiceWrapper, times(trackedCalls.size())) + .onCallAudioStateChanged(any(), any()); + } + + @SmallTest + @Test + public void testSystemAudioStateIsNotUpdatedFlagOff() { + CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine( + mContext, + mockCallsManager, + mockBluetoothRouteManager, + mockWiredHeadsetManager, + mockStatusBarNotifier, + mAudioServiceFactory, + CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT, + mThreadHandler.getLooper(), + Runnable::run /** do async stuff sync for test purposes */, + mCommunicationDeviceTracker, + mFeatureFlags); + stateMachine.setCallAudioManager(mockCallAudioManager); + + Set<Call> trackedCalls = new HashSet<>(Arrays.asList(fakeCall, fakeSelfManagedCall)); + when(mockCallsManager.getTrackedCalls()).thenReturn(trackedCalls); + when(mFeatureFlags.availableRoutesNeverUpdatedAfterSetSystemAudioState()).thenReturn(false); + + // start state --> ROUTE_EARPIECE + CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE, + CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER); + stateMachine.initialize(initState); + + stateMachine.setCallAudioManager(mockCallAudioManager); + + assertEquals(stateMachine.getCurrentCallAudioState().getRoute(), + CallAudioRouteStateMachine.ROUTE_EARPIECE); + + // ROUTE_EARPIECE --> ROUTE_SPEAKER + stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_SPEAKER, + CallAudioRouteStateMachine.SPEAKER_ON); + + stateMachine.sendMessageWithSessionInfo( + CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE); + waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + CallAudioState expectedCallAudioState = stateMachine.getLastKnownCallAudioState(); + // assert expected end state assertEquals(stateMachine.getCurrentCallAudioState().getRoute(), CallAudioRouteStateMachine.ROUTE_SPEAKER); // should update the audio route on all tracked calls ... verify(mockConnectionServiceWrapper, times(trackedCalls.size())) .onCallAudioStateChanged(any(), any()); + + assertNotEquals(expectedCallAudioState, stateMachine.getCurrentCallAudioState()); + } + + @SmallTest + @Test + public void testSystemAudioStateIsUpdatedFlagOn() { + CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine( + mContext, + mockCallsManager, + mockBluetoothRouteManager, + mockWiredHeadsetManager, + mockStatusBarNotifier, + mAudioServiceFactory, + CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT, + mThreadHandler.getLooper(), + Runnable::run /** do async stuff sync for test purposes */, + mCommunicationDeviceTracker, + mFeatureFlags); + stateMachine.setCallAudioManager(mockCallAudioManager); + + Set<Call> trackedCalls = new HashSet<>(Arrays.asList(fakeCall, fakeSelfManagedCall)); + when(mockCallsManager.getTrackedCalls()).thenReturn(trackedCalls); + when(mFeatureFlags.availableRoutesNeverUpdatedAfterSetSystemAudioState()).thenReturn(true); + + // start state --> ROUTE_EARPIECE + CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE, + CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER); + stateMachine.initialize(initState); + + stateMachine.setCallAudioManager(mockCallAudioManager); + + assertEquals(stateMachine.getCurrentCallAudioState().getRoute(), + CallAudioRouteStateMachine.ROUTE_EARPIECE); + + // ROUTE_EARPIECE --> ROUTE_SPEAKER + stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_SPEAKER, + CallAudioRouteStateMachine.SPEAKER_ON); + + stateMachine.sendMessageWithSessionInfo( + CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE); + + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + + CallAudioState expectedCallAudioState = stateMachine.getLastKnownCallAudioState(); + + // assert expected end state + assertEquals(stateMachine.getCurrentCallAudioState().getRoute(), + CallAudioRouteStateMachine.ROUTE_SPEAKER); + // should update the audio route on all tracked calls ... + verify(mockConnectionServiceWrapper, times(trackedCalls.size())) + .onCallAudioStateChanged(any(), any()); + + assertEquals(expectedCallAudioState, stateMachine.getCurrentCallAudioState()); } @MediumTest @@ -229,7 +343,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER, CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER); @@ -259,6 +374,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { foundValid = true; } assertTrue(foundValid); + verify(mockBluetoothRouteManager, timeout(1000L)).getBluetoothAudioConnectedDevice(); } @MediumTest @@ -274,7 +390,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false); when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true); @@ -297,14 +414,14 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioState expectedMiddleState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET, CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); verifyNewSystemCallAudioState(initState, expectedMiddleState); resetMocks(); stateMachine.sendMessageWithSessionInfo( CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); verifyNewSystemCallAudioState(expectedMiddleState, initState); } @@ -321,7 +438,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false); @@ -342,7 +460,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioState.ROUTE_EARPIECE, CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); verifyNewSystemCallAudioState(initState, expectedEndState); resetMocks(); stateMachine.sendMessageWithSessionInfo( @@ -350,7 +468,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { stateMachine.sendMessageWithSessionInfo( CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_PRESENT); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); assertEquals(expectedEndState, stateMachine.getCurrentCallAudioState()); } @@ -367,7 +485,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); Collection<BluetoothDevice> availableDevices = Collections.singleton(bluetoothDevice1); @@ -392,12 +511,12 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, null, availableDevices); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); verifyNewSystemCallAudioState(initState, expectedMidState); // clear out the handler state before resetting mocks in order to avoid introducing a // CallAudioState that has a null list of supported BT devices - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); resetMocks(); // Now, switch back to BT explicitly @@ -415,9 +534,9 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, bluetoothDevice1, availableDevices); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); // second wait needed for the BT_AUDIO_CONNECTED message - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); verifyNewSystemCallAudioState(expectedMidState, expectedEndState); stateMachine.sendMessageWithSessionInfo( @@ -427,9 +546,9 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { stateMachine.sendMessageWithSessionInfo( CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_PRESENT); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); // second wait needed for the BT_AUDIO_CONNECTED message - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); // Verify that we're still on bluetooth. assertEquals(expectedEndState, stateMachine.getCurrentCallAudioState()); } @@ -447,7 +566,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false); @@ -462,13 +582,13 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, CallAudioRouteStateMachine.RINGING_FOCUS); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); verify(mockBluetoothRouteManager, never()).connectBluetoothAudio(nullable(String.class)); stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, CallAudioRouteStateMachine.ACTIVE_FOCUS); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); verify(mockBluetoothRouteManager, times(1)).connectBluetoothAudio(nullable(String.class)); } @@ -485,7 +605,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); setInBandRing(false); when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false); @@ -499,7 +620,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.RINGING_FOCUS); // Wait for the state machine to finish transiting to ActiveEarpiece before hooking up // bluetooth mocks - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true); when(mockBluetoothRouteManager.getConnectedDevices()) @@ -508,7 +629,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.BLUETOOTH_DEVICE_LIST_CHANGED); stateMachine.sendMessageWithSessionInfo( CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_PRESENT); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); verify(mockBluetoothRouteManager, never()).connectBluetoothAudio(null); CallAudioState expectedEndState = new CallAudioState(false, @@ -519,13 +640,13 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, CallAudioRouteStateMachine.ACTIVE_FOCUS); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); verify(mockBluetoothRouteManager, times(1)).connectBluetoothAudio(null); when(mockBluetoothRouteManager.getBluetoothAudioConnectedDevice()) .thenReturn(bluetoothDevice1); stateMachine.sendMessage(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); // It is possible that this will be called twice from ActiveBluetoothRoute#enter. The extra // call to setBluetoothOn will trigger BT_AUDIO_CONNECTED, which also ends up invoking // CallAudioManager#onRingerModeChange. @@ -545,7 +666,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); List<BluetoothDevice> availableDevices = Arrays.asList(bluetoothDevice1, bluetoothDevice2, bluetoothDevice3); @@ -571,17 +693,80 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.USER_SWITCH_BLUETOOTH, 0, bluetoothDevice2.getAddress()); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); verify(mockBluetoothRouteManager).connectBluetoothAudio(bluetoothDevice2.getAddress()); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); CallAudioState expectedEndState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, bluetoothDevice2, availableDevices); - verifyNewSystemCallAudioState(initState, expectedEndState); + assertEquals(expectedEndState, stateMachine.getCurrentCallAudioState()); + } + + @SmallTest + @Test + public void testCallDisconnectedWhenAudioRoutedToBluetooth() { + when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true); + CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine( + mContext, + mockCallsManager, + mockBluetoothRouteManager, + mockWiredHeadsetManager, + mockStatusBarNotifier, + mAudioServiceFactory, + CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, + mThreadHandler.getLooper(), + Runnable::run /** do async stuff sync for test purposes */, + mCommunicationDeviceTracker, + mFeatureFlags); + stateMachine.setCallAudioManager(mockCallAudioManager); + List<BluetoothDevice> availableDevices = Arrays.asList(bluetoothDevice1); + + when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false); + when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false); + when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true); + when(mockBluetoothRouteManager.getConnectedDevices()).thenReturn(availableDevices); + when(mockBluetoothRouteManager.isInbandRingingEnabled()).thenReturn(true); + when(mFeatureFlags.transitRouteBeforeAudioDisconnectBt()).thenReturn(true); + doAnswer(invocation -> { + when(mockBluetoothRouteManager.getBluetoothAudioConnectedDevice()) + .thenReturn(bluetoothDevice1); + stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED); + return null; + }).when(mockBluetoothRouteManager).connectBluetoothAudio(bluetoothDevice1.getAddress()); + doAnswer(invocation -> { + when(mockBluetoothRouteManager.getBluetoothAudioConnectedDevice()) + .thenReturn(bluetoothDevice1); + stateMachine.sendMessageWithSessionInfo( + CallAudioRouteStateMachine.BT_AUDIO_DISCONNECTED); + return null; + }).when(mockBluetoothRouteManager).disconnectAudio(); + + CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH, + CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, null, + availableDevices); + stateMachine.initialize(initState); + + stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, + CallAudioRouteStateMachine.ACTIVE_FOCUS); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + + stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, + CallAudioRouteStateMachine.NO_FOCUS, bluetoothDevice1.getAddress()); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + + verify(mockBluetoothRouteManager).disconnectAudio(); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + CallAudioState expectedEndState = new CallAudioState(false, + CallAudioState.ROUTE_BLUETOOTH, + CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, + bluetoothDevice1, + availableDevices); + + assertEquals(expectedEndState, stateMachine.getCurrentCallAudioState()); } @SmallTest @@ -597,7 +782,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false); CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER, @@ -606,13 +792,13 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { // Raise a dock connect event. stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.CONNECT_DOCK); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); assertTrue(!stateMachine.isInActiveState()); verify(mockAudioManager, never()).setSpeakerphoneOn(eq(true)); // Raise a dock disconnect event. stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.DISCONNECT_DOCK); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); assertTrue(!stateMachine.isInActiveState()); verify(mockAudioManager, never()).setSpeakerphoneOn(eq(false)); } @@ -630,7 +816,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false); @@ -642,7 +829,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { // Switch to active, pretending that a call came in. stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, CallAudioRouteStateMachine.ACTIVE_FOCUS); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); // Make sure that we've successfully switched to the active speaker route and that we've // called setSpeakerOn @@ -666,7 +853,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); List<BluetoothDevice> availableDevices = Arrays.asList(bluetoothDevice1, bluetoothDevice2); @@ -688,28 +876,19 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { // Switch to active, pretending that a call came in. stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, CallAudioRouteStateMachine.ACTIVE_FOCUS); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); // Make sure that we've successfully switched to the active BT route and that we've // called connectAudio on the right device. verify(mockBluetoothRouteManager, atLeastOnce()) .connectBluetoothAudio(eq(bluetoothDevice1.getAddress())); assertTrue(stateMachine.isInActiveState()); - - // Switch to inactive, pretending that the call disconnected. - stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, - CallAudioRouteStateMachine.NO_FOCUS); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); - - // Make sure that we've successfully switched to the quiescent BT route - assertEquals(CallAudioState.ROUTE_BLUETOOTH, - stateMachine.getCurrentCallAudioState().getRoute()); - assertFalse(stateMachine.isInActiveState()); } @SmallTest @Test public void testSetAndClearEarpieceCommunicationDevice() { + when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true); CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine( mContext, mockCallsManager, @@ -720,7 +899,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); AudioDeviceInfo earpiece = mock(AudioDeviceInfo.class); @@ -743,7 +923,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { // Switch to active stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, CallAudioRouteStateMachine.ACTIVE_FOCUS); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); // Make sure that we've successfully switched to the active earpiece and that we set the // communication device. @@ -757,7 +937,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { // Route earpiece to speaker stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_SPEAKER, CallAudioRouteStateMachine.SPEAKER_ON); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); // Assert that communication device was cleared verify(mockAudioManager).clearCommunicationDevice(); @@ -766,18 +946,21 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { @SmallTest @Test public void testSetAndClearWiredHeadsetCommunicationDevice() { + when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true); verifySetAndClearHeadsetCommunicationDevice(AudioDeviceInfo.TYPE_WIRED_HEADSET); } @SmallTest @Test public void testSetAndClearUsbHeadsetCommunicationDevice() { + when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true); verifySetAndClearHeadsetCommunicationDevice(AudioDeviceInfo.TYPE_USB_HEADSET); } @SmallTest @Test public void testActiveFocusRouteSwitchFromQuiescentBluetooth() { + when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true); CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine( mContext, mockCallsManager, @@ -788,7 +971,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); // Start the route in quiescent and ensure that a switch to ACTIVE_FOCUS transitions to @@ -801,7 +985,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { // Switch to active stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, CallAudioRouteStateMachine.ACTIVE_FOCUS); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); // Make sure that we've successfully switched to the active route on BT assertTrue(stateMachine.isInActiveState()); @@ -893,7 +1077,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.initialize(); assertEquals(expectedState, stateMachine.getCurrentCallAudioState()); } @@ -911,7 +1096,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE, @@ -922,15 +1108,206 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioState expectedEndState = new CallAudioState(false, CallAudioState.ROUTE_STREAMING, CallAudioState.ROUTE_STREAMING); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); verifyNewSystemCallAudioState(initState, expectedEndState); resetMocks(); stateMachine.sendMessageWithSessionInfo( CallAudioRouteStateMachine.STREAMING_FORCE_DISABLED); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); assertEquals(initState, stateMachine.getCurrentCallAudioState()); } + @SmallTest + @Test + public void testIgnoreSpeakerOffMessage() { + when(mockBluetoothRouteManager.isInbandRingingEnabled()).thenReturn(true); + when(mockBluetoothRouteManager.getBluetoothAudioConnectedDevice()) + .thenReturn(bluetoothDevice1); + when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(true); + CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine( + mContext, + mockCallsManager, + mockBluetoothRouteManager, + mockWiredHeadsetManager, + mockStatusBarNotifier, + mAudioServiceFactory, + CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, + mThreadHandler.getLooper(), + Runnable::run /** do async stuff sync for test purposes */, + mCommunicationDeviceTracker, + mFeatureFlags); + stateMachine.setCallAudioManager(mockCallAudioManager); + + CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER, + CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER + | CallAudioState.ROUTE_BLUETOOTH); + stateMachine.initialize(initState); + + doAnswer( + (address) -> { + stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SPEAKER_OFF); + stateMachine.sendMessageDelayed(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED, + 5000L); + return null; + }).when(mockBluetoothRouteManager).connectBluetoothAudio(anyString()); + stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, + CallAudioRouteStateMachine.ACTIVE_FOCUS); + stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.USER_SWITCH_BLUETOOTH); + + CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER, + CallAudioState.ROUTE_SPEAKER | CallAudioState.ROUTE_BLUETOOTH + | CallAudioState.ROUTE_EARPIECE); + assertEquals(expectedState, stateMachine.getCurrentCallAudioState()); + } + + @MediumTest + @Test + public void testIgnoreImplicitBTSwitchWhenDeviceIsWatch() { + when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true); + when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true); + CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine( + mContext, + mockCallsManager, + mockBluetoothRouteManager, + mockWiredHeadsetManager, + mockStatusBarNotifier, + mAudioServiceFactory, + CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, + mThreadHandler.getLooper(), + Runnable::run /** do async stuff sync for test purposes */, + mCommunicationDeviceTracker, + mFeatureFlags); + stateMachine.setCallAudioManager(mockCallAudioManager); + + AudioDeviceInfo headset = mock(AudioDeviceInfo.class); + when(headset.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET); + when(headset.getAddress()).thenReturn(""); + List<AudioDeviceInfo> devices = new ArrayList<>(); + devices.add(headset); + + when(mockAudioManager.getAvailableCommunicationDevices()) + .thenReturn(devices); + when(mockAudioManager.setCommunicationDevice(eq(headset))) + .thenReturn(true); + when(mockAudioManager.getCommunicationDevice()).thenReturn(headset); + + CallAudioState initState = new CallAudioState(false, + CallAudioState.ROUTE_WIRED_HEADSET, CallAudioState.ROUTE_WIRED_HEADSET + | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH); + stateMachine.initialize(initState); + + // Switch to active + stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, + CallAudioRouteStateMachine.ACTIVE_FOCUS); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + + // Make sure that we've successfully switched to the active headset. + assertTrue(stateMachine.isInActiveState()); + ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor = ArgumentCaptor.forClass( + AudioDeviceInfo.class); + verify(mockAudioManager).setCommunicationDevice(infoArgumentCaptor.capture()); + assertEquals(AudioDeviceInfo.TYPE_WIRED_HEADSET, infoArgumentCaptor.getValue().getType()); + + // Set up watch device as only available BT device. + Collection<BluetoothDevice> availableDevices = Collections.singleton(mockWatchDevice); + + when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false); + when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true); + when(mockBluetoothRouteManager.getConnectedDevices()).thenReturn(availableDevices); + when(mockBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true); + + // Disconnect wired headset to force switch to BT (verify that we ignore the implicit switch + // to BT when the watch is the only connected device and that we move into the next + // available route. + stateMachine.sendMessageWithSessionInfo( + CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE, + CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, + null, availableDevices); + assertEquals(expectedState, stateMachine.getCurrentCallAudioState()); + } + + @SmallTest + @Test + public void testQuiescentBluetoothRouteResetMute() { + when(mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()).thenReturn(true); + CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine( + mContext, + mockCallsManager, + mockBluetoothRouteManager, + mockWiredHeadsetManager, + mockStatusBarNotifier, + mAudioServiceFactory, + CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, + mThreadHandler.getLooper(), + Runnable::run /** do async stuff sync for test purposes */, + mCommunicationDeviceTracker, + mFeatureFlags); + stateMachine.setCallAudioManager(mockCallAudioManager); + + CallAudioState initState = new CallAudioState(false, + CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER + | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH); + stateMachine.initialize(initState); + + // Switch to active and mute + stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, + CallAudioRouteStateMachine.ACTIVE_FOCUS); + stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + assertTrue(stateMachine.isInActiveState()); + + stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.MUTE_ON); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + CallAudioState expectedState = new CallAudioState(true, + CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER + | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH); + assertEquals(expectedState, stateMachine.getCurrentCallAudioState()); + + stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, + CallAudioRouteStateMachine.NO_FOCUS); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + + expectedState = new CallAudioState(false, + CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER + | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH); + // TODO: Re-enable this part of the test; this is now failing because we have to + // revert ag/23783145. + // assertEquals(expectedState, stateMachine.getCurrentCallAudioState()); + } + + @SmallTest + @Test + public void testSupportRouteMaskUpdateWhenBtAudioConnected() { + when(mFeatureFlags.updateRouteMaskWhenBtConnected()).thenReturn(true); + CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine( + mContext, + mockCallsManager, + mockBluetoothRouteManager, + mockWiredHeadsetManager, + mockStatusBarNotifier, + mAudioServiceFactory, + CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, + mThreadHandler.getLooper(), + Runnable::run /** do async stuff sync for test purposes */, + mCommunicationDeviceTracker, + mFeatureFlags); + stateMachine.setCallAudioManager(mockCallAudioManager); + + CallAudioState initState = new CallAudioState(false, + CallAudioState.ROUTE_EARPIECE, + CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER); + stateMachine.initialize(initState); + + stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH, + CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER + | CallAudioState.ROUTE_BLUETOOTH); + assertEquals(expectedState, stateMachine.getCurrentCallAudioState()); + } + private void initializationTestHelper(CallAudioState expectedState, int earpieceControl) { when(mockWiredHeadsetManager.isPluggedIn()).thenReturn( @@ -950,7 +1327,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { earpieceControl, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.initialize(); assertEquals(expectedState, stateMachine.getCurrentCallAudioState()); } @@ -1002,7 +1380,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mThreadHandler.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); AudioDeviceInfo headset = mock(AudioDeviceInfo.class); @@ -1025,7 +1404,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { // Switch to active stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, CallAudioRouteStateMachine.ACTIVE_FOCUS); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); // Make sure that we've successfully switched to the active headset and that we set the // communication device. @@ -1039,7 +1418,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase { stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS, CallAudioRouteStateMachine.ACTIVE_FOCUS); stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.USER_SWITCH_EARPIECE); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); // Assert that communication device was cleared verify(mockAudioManager).clearCommunicationDevice(); diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java index 804ef17cd..25c4e9f15 100644 --- a/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java +++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java @@ -276,7 +276,8 @@ public class CallAudioRouteTransitionTests extends TelecomTestCase { mParams.earpieceControl, mHandlerThread.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); setupMocksForParams(stateMachine, mParams); @@ -294,17 +295,17 @@ public class CallAudioRouteTransitionTests extends TelecomTestCase { if (mParams.initialRoute == CallAudioState.ROUTE_BLUETOOTH) { stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED); } - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); // Clear invocations on mocks to discard stuff from initialization clearInvocations(); sendActionToStateMachine(stateMachine); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); - Handler h = stateMachine.getHandler(); + Handler h = stateMachine.getAdapterHandler(); waitForHandlerAction(h, TEST_TIMEOUT); stateMachine.quitStateMachine(); @@ -374,7 +375,8 @@ public class CallAudioRouteTransitionTests extends TelecomTestCase { mParams.earpieceControl, mHandlerThread.getLooper(), Runnable::run /** do async stuff sync for test purposes */, - mCommunicationDeviceTracker); + mCommunicationDeviceTracker, + mFeatureFlags); stateMachine.setCallAudioManager(mockCallAudioManager); // Set up bluetooth and speakerphone state @@ -395,8 +397,8 @@ public class CallAudioRouteTransitionTests extends TelecomTestCase { // Omit the focus-getting statement sendActionToStateMachine(stateMachine); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); - waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); + waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT); stateMachine.quitStateMachine(); diff --git a/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java index f4008aa83..9101a19eb 100644 --- a/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java +++ b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java @@ -17,6 +17,7 @@ package com.android.server.telecom.tests; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -50,7 +51,9 @@ import org.mockito.Mock; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; @RunWith(JUnit4.class) public class CallEndpointControllerTest extends TelecomTestCase { @@ -81,6 +84,9 @@ public class CallEndpointControllerTest extends TelecomTestCase { availableBluetooth1); private static final CallAudioState audioState7 = new CallAudioState(false, CallAudioState.ROUTE_STREAMING, CallAudioState.ROUTE_ALL, null, availableBluetooth1); + private static final CallAudioState audioState8 = new CallAudioState(false, + CallAudioState.ROUTE_EARPIECE, CallAudioState.ROUTE_ALL, bluetoothDevice1, + availableBluetooth2); private CallEndpointController mCallEndpointController; @@ -177,6 +183,74 @@ public class CallEndpointControllerTest extends TelecomTestCase { verify(mConnectionService, never()).onMuteStateChanged(any(), anyBoolean()); } + /** + * Ensure that {@link CallAudioManager#setAudioRoute(int, String)} is invoked when the user + * requests to switch to a bluetooth CallEndpoint. This is an edge case where bluetooth is not + * the current CallEndpoint but the CallAudioState shows the bluetooth device is + * active/available. + */ + @Test + public void testSwitchFromEarpieceToBluetooth() { + // simulate an audio state where the EARPIECE is active but a bluetooth device is active. + mCallEndpointController.onCallAudioStateChanged(null, audioState8 /* Ear but BT active */); + CallEndpoint btEndpoint = mCallEndpointController.getAvailableEndpoints().stream() + .filter(e -> e.getEndpointType() == CallEndpoint.TYPE_BLUETOOTH) + .toList().get(0); // get the only available BT endpoint + + // verify the CallEndpointController shows EARPIECE active + BT endpoint is active device + assertEquals(CallEndpoint.TYPE_EARPIECE, + mCallEndpointController.getCurrentCallEndpoint().getEndpointType()); + assertNotNull(btEndpoint); + + // request an endpoint change from earpiece to the bluetooth + doReturn(audioState8).when(mCallAudioManager).getCallAudioState(); + mCallEndpointController.requestCallEndpointChange(btEndpoint, mResultReceiver); + + // verify the transaction was successful and CallAudioManager#setAudioRoute was called + verify(mResultReceiver, never()).send(eq(CallEndpoint.ENDPOINT_OPERATION_FAILED), any()); + verify(mCallAudioManager, times(1)).setAudioRoute(eq(CallAudioState.ROUTE_BLUETOOTH), + eq(bluetoothDevice1.getAddress())); + } + + + /** + * Ensure that {@link CallAudioManager#setAudioRoute(int, String)} is invoked when the user + * requests to switch to from one bluetooth device to another. + */ + @Test + public void testBtDeviceSwitch() { + // bluetoothDevice1 should start as active and bluetoothDevice2 is available + mCallEndpointController.onCallAudioStateChanged(null, audioState2 /* BT active D1 */); + CallEndpoint currentEndpoint = mCallEndpointController.getCurrentCallEndpoint(); + List<CallEndpoint> btEndpoints = mCallEndpointController.getAvailableEndpoints().stream() + .filter(e -> e.getEndpointType() == CallEndpoint.TYPE_BLUETOOTH) + .toList(); // get the only available BT endpoint + + // verify the initial state of the test + assertEquals(2, btEndpoints.size()); + assertEquals(CallEndpoint.TYPE_BLUETOOTH, currentEndpoint.getEndpointType()); + + CallEndpoint otherBluetoothEndpoint = null; + for (CallEndpoint e : btEndpoints) { + if (!e.equals(currentEndpoint)) { + otherBluetoothEndpoint = e; + } + } + + assertNotNull(otherBluetoothEndpoint); + assertNotEquals(currentEndpoint, otherBluetoothEndpoint); + + // request an endpoint change from BT D1 --> BT D2 + doReturn(audioState2).when(mCallAudioManager).getCallAudioState(); + mCallEndpointController.requestCallEndpointChange(otherBluetoothEndpoint, mResultReceiver); + + // verify the transaction was successful and CallAudioManager#setAudioRoute was called + verify(mResultReceiver, never()).send(eq(CallEndpoint.ENDPOINT_OPERATION_FAILED), any()); + verify(mCallAudioManager, times(1)) + .setAudioRoute(eq(CallAudioState.ROUTE_BLUETOOTH), + eq(bluetoothDevice2.getAddress())); + } + @Test public void testAvailableEndpointChanged() throws Exception { mCallEndpointController.onCallAudioStateChanged(audioState1, audioState6); diff --git a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java index 946622030..c09d138d5 100644 --- a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java +++ b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -36,6 +37,7 @@ import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; +import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.content.res.Resources; import android.location.Country; @@ -65,6 +67,7 @@ import android.test.suitebuilder.annotation.SmallTest; import androidx.test.filters.FlakyTest; import com.android.server.telecom.Analytics; +import com.android.server.telecom.AnomalyReporterAdapter; import com.android.server.telecom.Call; import com.android.server.telecom.CallLogManager; import com.android.server.telecom.CallState; @@ -72,6 +75,7 @@ import com.android.server.telecom.HandoverState; import com.android.server.telecom.MissedCallNotifier; import com.android.server.telecom.PhoneAccountRegistrar; import com.android.server.telecom.TelephonyUtil; +import com.android.server.telecom.flags.FeatureFlags; import org.junit.After; import org.junit.Before; @@ -123,6 +127,11 @@ public class CallLogManagerTest extends TelecomTestCase { PhoneAccountRegistrar mMockPhoneAccountRegistrar; @Mock MissedCallNotifier mMissedCallNotifier; + @Mock + AnomalyReporterAdapter mAnomalyReporterAdapter; + + @Mock + FeatureFlags mFeatureFlags; @Override @Before @@ -130,7 +139,7 @@ public class CallLogManagerTest extends TelecomTestCase { super.setUp(); mContext = mComponentContextFixture.getTestDouble().getApplicationContext(); mCallLogManager = new CallLogManager(mContext, mMockPhoneAccountRegistrar, - mMissedCallNotifier); + mMissedCallNotifier, mAnomalyReporterAdapter, mFeatureFlags); mDefaultAccountHandle = new PhoneAccountHandle( new ComponentName("com.android.server.telecom.tests", "CallLogManagerTest"), TEST_PHONE_ACCOUNT_ID, @@ -181,6 +190,9 @@ public class CallLogManagerTest extends TelecomTestCase { when(userManager.getUserInfo(eq(CURRENT_USER_ID))).thenReturn(userInfo); when(userManager.getUserInfo(eq(OTHER_USER_ID))).thenReturn(otherUserInfo); when(userManager.getUserInfo(eq(MANAGED_USER_ID))).thenReturn(managedProfileUserInfo); + PackageManager packageManager = mContext.getPackageManager(); + when(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false); + when(mFeatureFlags.telecomLogExternalWearableCalls()).thenReturn(false); } @Override @@ -357,6 +369,7 @@ public class CallLogManagerTest extends TelecomTestCase { VIA_NUMBER_STRING, // viaNumber null ); + when(mFeatureFlags.addCallUriForMissedCalls()).thenReturn(true); mCallLogManager.onCallStateChanged(fakeIncomingCall, CallState.ACTIVE, CallState.DISCONNECTED); ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID); @@ -366,7 +379,7 @@ public class CallLogManagerTest extends TelecomTestCase { @MediumTest @Test - public void testLogCallDirectionMissed() { + public void testLogCallDirectionMissedAddCallUriForMissedCallsFlagOff() { when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class))) .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID)); Call fakeMissedCall = makeFakeCall( @@ -382,6 +395,7 @@ public class CallLogManagerTest extends TelecomTestCase { VIA_NUMBER_STRING, // viaNumber null ); + when(mFeatureFlags.addCallUriForMissedCalls()).thenReturn(false); mCallLogManager.onCallStateChanged(fakeMissedCall, CallState.ACTIVE, CallState.DISCONNECTED); @@ -390,7 +404,39 @@ public class CallLogManagerTest extends TelecomTestCase { Integer.valueOf(CallLog.Calls.MISSED_TYPE)); // Timeout needed because showMissedCallNotification is called from onPostExecute. verify(mMissedCallNotifier, timeout(TEST_TIMEOUT_MILLIS)) - .showMissedCallNotification(any(MissedCallNotifier.CallInfo.class)); + .showMissedCallNotification(any(MissedCallNotifier.CallInfo.class), + /* uri= */ eq(null)); + } + + @MediumTest + @Test + public void testLogCallDirectionMissedAddCallUriForMissedCallsFlagOn() { + when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class))) + .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID)); + Call fakeMissedCall = makeFakeCall( + DisconnectCause.MISSED, // disconnectCauseCode + false, // isConference + true, // isIncoming + 1L, // creationTimeMillis + 1000L, // ageMillis + TEL_PHONEHANDLE, // callHandle + mDefaultAccountHandle, // phoneAccountHandle + NO_VIDEO_STATE, // callVideoState + POST_DIAL_STRING, // postDialDigits + VIA_NUMBER_STRING, // viaNumber + null + ); + when(mFeatureFlags.addCallUriForMissedCalls()).thenReturn(true); + + mCallLogManager.onCallStateChanged(fakeMissedCall, CallState.ACTIVE, + CallState.DISCONNECTED); + ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID); + assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE), + Integer.valueOf(CallLog.Calls.MISSED_TYPE)); + // Timeout needed because showMissedCallNotification is called from onPostExecute. + verify(mMissedCallNotifier, timeout(TEST_TIMEOUT_MILLIS)) + .showMissedCallNotification(any(MissedCallNotifier.CallInfo.class), + /* uri= */ any(Uri.class)); } @MediumTest @@ -788,6 +834,34 @@ public class CallLogManagerTest extends TelecomTestCase { assertEquals(1, insertedValues.getAsInteger(Calls.IS_READ).intValue()); } + @Test + public void testLogCallWhenExternalCallOnWatch() { + when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class))) + .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID)); + PackageManager packageManager = mContext.getPackageManager(); + when(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true); + when(mFeatureFlags.telecomLogExternalWearableCalls()).thenReturn(true); + Call fakeMissedCall = makeFakeCall( + DisconnectCause.REJECTED, // disconnectCauseCode + false, // isConference + true, // isIncoming + 1L, // creationTimeMillis + 1000L, // ageMillis + TEL_PHONEHANDLE, // callHandle + mDefaultAccountHandle, // phoneAccountHandle + NO_VIDEO_STATE, // callVideoState + POST_DIAL_STRING, // postDialDigits + VIA_NUMBER_STRING, // viaNumber + null + ); + when(fakeMissedCall.isExternalCall()).thenReturn(true); + + mCallLogManager.onCallStateChanged(fakeMissedCall, CallState.ACTIVE, + CallState.DISCONNECTED); + verifyInsertionWithCapture(CURRENT_USER_ID); + } + + @SmallTest @Test public void testCountryIso_setCache() { @@ -893,6 +967,56 @@ public class CallLogManagerTest extends TelecomTestCase { @SmallTest @Test + public void testDoNotLogCallExtra() { + when(mFeatureFlags.telecomSkipLogBasedOnExtra()).thenReturn(true); + Call fakeCall = makeFakeCall( + DisconnectCause.LOCAL, // disconnectCauseCode + false, // isConference + true, // isIncoming + 1L, // creationTimeMillis + 1000L, // ageMillis + TEL_PHONEHANDLE, // callHandle + mDefaultAccountHandle, // phoneAccountHandle + NO_VIDEO_STATE, // callVideoState + POST_DIAL_STRING, // postDialDigits + VIA_NUMBER_STRING, // viaNumber + UserHandle.of(CURRENT_USER_ID) + ); + Bundle extras = new Bundle(); + extras.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true); + when(fakeCall.getExtras()).thenReturn(extras); + + assertFalse(mCallLogManager.shouldLogDisconnectedCall(fakeCall, CallState.DISCONNECTED, + false /* isCanceled */)); + } + + @SmallTest + @Test + public void testIgnoresDoNotLogCallExtra_whenFlagDisabled() { + when(mFeatureFlags.telecomSkipLogBasedOnExtra()).thenReturn(false); + Call fakeCall = makeFakeCall( + DisconnectCause.LOCAL, // disconnectCauseCode + false, // isConference + true, // isIncoming + 1L, // creationTimeMillis + 1000L, // ageMillis + TEL_PHONEHANDLE, // callHandle + mDefaultAccountHandle, // phoneAccountHandle + NO_VIDEO_STATE, // callVideoState + POST_DIAL_STRING, // postDialDigits + VIA_NUMBER_STRING, // viaNumber + UserHandle.of(CURRENT_USER_ID) + ); + Bundle extras = new Bundle(); + extras.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true); + when(fakeCall.getExtras()).thenReturn(extras); + + assertTrue(mCallLogManager.shouldLogDisconnectedCall(fakeCall, CallState.DISCONNECTED, + false /* isCanceled */)); + } + + @SmallTest + @Test public void testDoNotLogConferenceWithChildren() { Call fakeCall = makeFakeCall( DisconnectCause.LOCAL, // disconnectCauseCode diff --git a/tests/src/com/android/server/telecom/tests/CallTest.java b/tests/src/com/android/server/telecom/tests/CallTest.java index 997e7dd2a..7a773748a 100644 --- a/tests/src/com/android/server/telecom/tests/CallTest.java +++ b/tests/src/com/android/server/telecom/tests/CallTest.java @@ -21,7 +21,6 @@ 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.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.argThat; @@ -34,10 +33,12 @@ import static org.mockito.Mockito.verify; import android.content.ComponentName; import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.Bundle; +import android.os.UserHandle; import android.telecom.CallAttributes; import android.telecom.CallerInfo; import android.telecom.Connection; @@ -117,6 +118,7 @@ public class CallTest extends TelecomTestCase { doReturn(new ComponentName(mContext, CallTest.class)) .when(mMockConnectionService).getComponentName(); doReturn(mMockToast).when(mMockToastProxy).makeText(any(), anyInt(), anyInt()); + doReturn(UserHandle.CURRENT).when(mMockCallsManager).getCurrentUserHandle(); } @After @@ -200,7 +202,8 @@ public class CallTest extends TelecomTestCase { false /* shouldAttachToExistingConnection*/, false /* isConference */, mMockClockProxy, - mMockToastProxy); + mMockToastProxy, + mFeatureFlags); // To start with connection creation isn't complete. assertFalse(call.isCreateConnectionComplete()); @@ -338,7 +341,8 @@ public class CallTest extends TelecomTestCase { false /* shouldAttachToExistingConnection*/, true /* isConference */, mMockClockProxy, - mMockToastProxy); + mMockToastProxy, + mFeatureFlags); assertFalse(call.wasDndCheckComputedForCall()); assertFalse(call.isCallSuppressedByDoNotDisturb()); @@ -364,7 +368,8 @@ public class CallTest extends TelecomTestCase { false /* shouldAttachToExistingConnection*/, true /* isConference */, mMockClockProxy, - mMockToastProxy); + mMockToastProxy, + mFeatureFlags); assertNull(call.getConnectionServiceWrapper()); assertFalse(call.isTransactionalCall()); @@ -394,7 +399,8 @@ public class CallTest extends TelecomTestCase { false /* shouldAttachToExistingConnection*/, true /* isConference */, mMockClockProxy, - mMockToastProxy); + mMockToastProxy, + mFeatureFlags); // setup call.setIsTransactionalCall(true); @@ -728,6 +734,52 @@ public class CallTest extends TelecomTestCase { })); } + @Test + @SmallTest + public void testExcludesInCallServiceFromDoNotLogCallExtra() { + Call call = createCall("any"); + Bundle extra = new Bundle(); + extra.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true); + + call.putInCallServiceExtras(extra, "packageName"); + + assertFalse(call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL)); + } + + @Test + @SmallTest + public void testExcludesConnectionServiceWithoutModifyStatePermissionFromDoNotLogCallExtra() { + PackageManager packageManager = mContext.getPackageManager(); + Bundle extra = new Bundle(); + extra.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true); + String packageName = SIM_1_HANDLE.getComponentName().getPackageName(); + doReturn(PackageManager.PERMISSION_DENIED) + .when(packageManager) + .checkPermission(android.Manifest.permission.MODIFY_PHONE_STATE, packageName); + Call call = createCall("any"); + + call.putConnectionServiceExtras(extra); + + assertFalse(call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL)); + } + + @Test + @SmallTest + public void testDoesNotExcludeConnectionServiceWithModifyStatePermissionFromDoNotLogCallExtra() { + String packageName = SIM_1_HANDLE.getComponentName().getPackageName(); + Bundle extra = new Bundle(); + extra.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true); + PackageManager packageManager = mContext.getPackageManager(); + doReturn(PackageManager.PERMISSION_GRANTED) + .when(packageManager) + .checkPermission(android.Manifest.permission.MODIFY_PHONE_STATE, packageName); + Call call = createCall("any"); + + call.putConnectionServiceExtras(extra); + + assertTrue(call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL)); + } + private Call createCall(String id) { return createCall(id, Call.CALL_DIRECTION_UNDEFINED); } @@ -748,6 +800,7 @@ public class CallTest extends TelecomTestCase { false, false, mMockClockProxy, - mMockToastProxy); + mMockToastProxy, + mFeatureFlags); } } diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java index 00be89f50..649f435f3 100644 --- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java +++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java @@ -17,10 +17,8 @@ package com.android.server.telecom.tests; import static android.provider.CallLog.Calls.USER_MISSED_NOT_RUNNING; - import static junit.framework.Assert.assertNotNull; import static junit.framework.TestCase.fail; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -43,6 +41,7 @@ import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static java.lang.Thread.sleep; import android.Manifest; import android.content.ComponentName; @@ -54,6 +53,7 @@ import android.media.AudioManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.OutcomeReceiver; import android.os.Process; @@ -61,6 +61,7 @@ import android.os.ResultReceiver; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.BlockedNumberContract; import android.telecom.CallException; import android.telecom.CallScreeningService; @@ -80,6 +81,7 @@ import android.test.suitebuilder.annotation.SmallTest; import android.util.Pair; import android.widget.Toast; +import com.android.internal.telecom.IConnectionService; import com.android.server.telecom.AnomalyReporterAdapter; import com.android.server.telecom.AsyncRingtonePlayer; import com.android.server.telecom.Call; @@ -98,6 +100,7 @@ import com.android.server.telecom.ClockProxy; import com.android.server.telecom.ConnectionServiceFocusManager; import com.android.server.telecom.ConnectionServiceFocusManager.ConnectionServiceFocusManagerFactory; import com.android.server.telecom.ConnectionServiceWrapper; +import com.android.server.telecom.CreateConnectionResponse; import com.android.server.telecom.DefaultDialerCache; import com.android.server.telecom.EmergencyCallDiagnosticLogger; import com.android.server.telecom.EmergencyCallHelper; @@ -124,6 +127,9 @@ import com.android.server.telecom.bluetooth.BluetoothRouteManager; import com.android.server.telecom.bluetooth.BluetoothStateReceiver; import com.android.server.telecom.callfiltering.BlockedNumbersAdapter; import com.android.server.telecom.callfiltering.CallFilteringResult; +import com.android.server.telecom.callfiltering.IncomingCallFilterGraph; +import com.android.server.telecom.flags.FeatureFlags; +import com.android.server.telecom.flags.Flags; import com.android.server.telecom.ui.AudioProcessingNotification; import com.android.server.telecom.ui.CallStreamingNotification; import com.android.server.telecom.ui.DisconnectedCallNotifier; @@ -132,6 +138,7 @@ import com.android.server.telecom.voip.TransactionManager; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -279,7 +286,11 @@ public class CallsManagerTest extends TelecomTestCase { @Mock private PhoneCapability mPhoneCapability; @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker; @Mock private CallStreamingNotification mCallStreamingNotification; + @Mock private FeatureFlags mFeatureFlags; + @Mock private IncomingCallFilterGraph mIncomingCallFilterGraph; + @Mock private IConnectionService mIConnectionService; + @Rule public SetFlagsRule mSetRlagsRule = new SetFlagsRule(); private CallsManager mCallsManager; @Override @@ -298,8 +309,8 @@ public class CallsManagerTest extends TelecomTestCase { when(mCallEndpointControllerFactory.create(any(), any(), any())).thenReturn( mCallEndpointController); when(mCallAudioRouteStateMachineFactory.create(any(), any(), any(), any(), any(), any(), - anyInt(), any(), any())).thenReturn(mCallAudioRouteStateMachine); - when(mCallAudioModeStateMachineFactory.create(any(), any())) + anyInt(), any(), any(), any())).thenReturn(mCallAudioRouteStateMachine); + when(mCallAudioModeStateMachineFactory.create(any(), any(), any(), any())) .thenReturn(mCallAudioModeStateMachine); when(mClockProxy.currentTimeMillis()).thenReturn(System.currentTimeMillis()); when(mClockProxy.elapsedRealtime()).thenReturn(SystemClock.elapsedRealtime()); @@ -353,7 +364,9 @@ public class CallsManagerTest extends TelecomTestCase { TransactionManager.getTestInstance(), mEmergencyCallDiagnosticLogger, mCommunicationDeviceTracker, - mCallStreamingNotification); + mCallStreamingNotification, + mFeatureFlags, + (call, listener, context, timeoutsAdapter, lock) -> mIncomingCallFilterGraph); when(mPhoneAccountRegistrar.getPhoneAccount( eq(SELF_MANAGED_HANDLE), any())).thenReturn(SELF_MANAGED_ACCOUNT); @@ -365,11 +378,17 @@ public class CallsManagerTest extends TelecomTestCase { eq(WORK_HANDLE), any())).thenReturn(WORK_ACCOUNT); when(mToastFactory.makeText(any(), anyInt(), anyInt())).thenReturn(mToast); when(mToastFactory.makeText(any(), any(), anyInt())).thenReturn(mToast); + when(mIConnectionService.asBinder()).thenReturn(mock(IBinder.class)); + + mComponentContextFixture.addConnectionService( + SIM_1_ACCOUNT.getAccountHandle().getComponentName(), mIConnectionService); } @Override @After public void tearDown() throws Exception { + mComponentContextFixture.removeConnectionService( + SIM_1_ACCOUNT.getAccountHandle().getComponentName(), mIConnectionService); super.tearDown(); } @@ -396,7 +415,8 @@ public class CallsManagerTest extends TelecomTestCase { false /* shouldAttachToExistingConnection*/, false /* isConference */, mClockProxy, - mToastFactory); + mToastFactory, + mFeatureFlags); ongoingCall.setState(CallState.ACTIVE, "just cuz"); return ongoingCall; } @@ -1323,8 +1343,9 @@ public class CallsManagerTest extends TelecomTestCase { @SmallTest @Test - public void testNoFilteringOfCallsWhenPhoneAccountRequestsSkipped() { + public void testDndFilterAppliesOfCallsWhenPhoneAccountRequestsSkipped() { // GIVEN an incoming call which is from a PhoneAccount that requested to skip filtering. + when(mFeatureFlags.skipFilterPhoneAccountPerformDndFilter()).thenReturn(true); Call incomingCall = addSpyCall(SIM_1_HANDLE, CallState.NEW); Bundle extras = new Bundle(); extras.putBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING, true); @@ -1344,7 +1365,35 @@ public class CallsManagerTest extends TelecomTestCase { // WHEN the incoming call is successfully added. mCallsManager.onSuccessfulIncomingCall(incomingCall); - // THEN the incoming call is not using call filtering + // THEN the incoming call is still applying Dnd filter. + verify(incomingCall).setIsUsingCallFiltering(eq(true)); + } + + @SmallTest + @Test + public void testNoFilterAppliesOfCallsWhenFlagNotEnabled() { + // Flag is not enabled. + when(mFeatureFlags.skipFilterPhoneAccountPerformDndFilter()).thenReturn(false); + Call incomingCall = addSpyCall(SIM_1_HANDLE, CallState.NEW); + Bundle extras = new Bundle(); + extras.putBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING, true); + PhoneAccount skipRequestedAccount = new PhoneAccount.Builder(SIM_2_HANDLE, "Skipper") + .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION + | PhoneAccount.CAPABILITY_CALL_PROVIDER) + .setExtras(extras) + .setIsEnabled(true) + .build(); + when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(SIM_1_HANDLE)) + .thenReturn(skipRequestedAccount); + doReturn(false).when(incomingCall).can(Connection.CAPABILITY_HOLD); + doReturn(false).when(incomingCall).can(Connection.CAPABILITY_SUPPORT_HOLD); + doReturn(false).when(incomingCall).isSelfManaged(); + doReturn(true).when(incomingCall).setState(anyInt(), any()); + + // WHEN the incoming call is successfully added. + mCallsManager.onSuccessfulIncomingCall(incomingCall); + + // THEN the incoming call is not applying filter. verify(incomingCall).setIsUsingCallFiltering(eq(false)); } @@ -2401,7 +2450,7 @@ public class CallsManagerTest extends TelecomTestCase { mCallsManager.onCallFilteringComplete(callSpy, result, false /* timeout */); verify(mMissedCallNotifier).showMissedCallNotification( - any(MissedCallNotifier.CallInfo.class)); + any(MissedCallNotifier.CallInfo.class), /* uri= */ eq(null)); } @Test @@ -2505,6 +2554,32 @@ public class CallsManagerTest extends TelecomTestCase { assertEquals(DEFAULT_CALL_SCREENING_APP, outgoingCall.getPostCallPackageName()); } + /** + * Verify the only call state set from calling onSuccessfulOutgoingCall is CallState.DIALING. + */ + @SmallTest + @Test + public void testOutgoingCallStateIsSetToAPreviousStateAndIgnored() { + when(mFeatureFlags.fixAudioFlickerForOutgoingCalls()).thenReturn(true); + Call outgoingCall = addSpyCall(CallState.CONNECTING); + mCallsManager.onSuccessfulOutgoingCall(outgoingCall, CallState.NEW); + verify(outgoingCall, never()).setState(eq(CallState.NEW), any()); + verify(outgoingCall, times(1)).setState(eq(CallState.DIALING), any()); + } + + /** + * Verify a ConnectionService can start the call in the active state and avoid the dialing state + */ + @SmallTest + @Test + public void testOutgoingCallStateCanAvoidDialingAndGoStraightToActive() { + when(mFeatureFlags.fixAudioFlickerForOutgoingCalls()).thenReturn(true); + Call outgoingCall = addSpyCall(CallState.CONNECTING); + mCallsManager.onSuccessfulOutgoingCall(outgoingCall, CallState.ACTIVE); + verify(outgoingCall, never()).setState(eq(CallState.DIALING), any()); + verify(outgoingCall, times(1)).setState(eq(CallState.ACTIVE), any()); + } + @SmallTest @Test public void testRejectIncomingCallOnPAHInactive_SecondaryUser() throws Exception { @@ -2514,9 +2589,7 @@ public class CallsManagerTest extends TelecomTestCase { WORK_HANDLE.getUserHandle(), service); UserManager um = mContext.getSystemService(UserManager.class); - UserHandle newUser = new UserHandle(11); - when(mCallsManager.getCurrentUserHandle()).thenReturn(newUser); - when(um.isUserAdmin(eq(newUser.getIdentifier()))).thenReturn(false); + when(um.isUserAdmin(anyInt())).thenReturn(false); when(um.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle()))).thenReturn(false); when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(eq(WORK_HANDLE))) .thenReturn(WORK_ACCOUNT); @@ -2532,14 +2605,17 @@ public class CallsManagerTest extends TelecomTestCase { @Test public void testRejectIncomingCallOnPAHInactive_ProfilePaused() throws Exception { ConnectionServiceWrapper service = mock(ConnectionServiceWrapper.class); - doReturn(SIM_2_HANDLE.getComponentName()).when(service).getComponentName(); - mCallsManager.addConnectionServiceRepositoryCache(SIM_2_HANDLE.getComponentName(), - SIM_2_HANDLE.getUserHandle(), service); + doReturn(WORK_HANDLE.getComponentName()).when(service).getComponentName(); + mCallsManager.addConnectionServiceRepositoryCache(WORK_HANDLE.getComponentName(), + WORK_HANDLE.getUserHandle(), service); UserManager um = mContext.getSystemService(UserManager.class); - when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true); + when(um.isUserAdmin(anyInt())).thenReturn(true); + when(um.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle()))).thenReturn(true); + when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(eq(WORK_HANDLE))) + .thenReturn(WORK_ACCOUNT); Call newCall = mCallsManager.processIncomingCallIntent( - SIM_2_HANDLE, new Bundle(), false); + WORK_HANDLE, new Bundle(), false); verify(service, timeout(TEST_TIMEOUT)).createConnectionFailed(any()); assertFalse(newCall.isInECBM()); @@ -2576,9 +2652,7 @@ public class CallsManagerTest extends TelecomTestCase { when(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(eq(WORK_HANDLE))) .thenReturn(true); UserManager um = mContext.getSystemService(UserManager.class); - UserHandle newUser = new UserHandle(11); - when(mCallsManager.getCurrentUserHandle()).thenReturn(newUser); - when(um.isUserAdmin(eq(newUser.getIdentifier()))).thenReturn(false); + when(um.isUserAdmin(anyInt())).thenReturn(false); when(um.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle()))).thenReturn(false); when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(eq(WORK_HANDLE))) .thenReturn(WORK_ACCOUNT); @@ -2762,6 +2836,36 @@ public class CallsManagerTest extends TelecomTestCase { assertTrue(result.contains("onReceiveResult")); } + @Test + public void testConnectionServiceCreateConnectionTimeout() throws Exception { + mSetRlagsRule.enableFlags(Flags.FLAG_UNBIND_TIMEOUT_CONNECTIONS); + ConnectionServiceWrapper service = new ConnectionServiceWrapper( + SIM_1_ACCOUNT.getAccountHandle().getComponentName(), null, + mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null); + TestScheduledExecutorService scheduledExecutorService = new TestScheduledExecutorService(); + service.setScheduledExecutorService(scheduledExecutorService); + Call call = addSpyCall(); + service.addCall(call); + when(call.isCreateConnectionComplete()).thenReturn(false); + CreateConnectionResponse response = mock(CreateConnectionResponse.class); + + service.createConnection(call, response); + waitUntilConditionIsTrueOrTimeout(new Condition() { + @Override + public Object expected() { + return true; + } + + @Override + public Object actual() { + return scheduledExecutorService.isRunnableScheduledAtTime(15000L); + } + }, 5000L, "Expected job failed to schedule"); + scheduledExecutorService.advanceTime(15000L); + verify(response).handleCreateConnectionFailure( + eq(new DisconnectCause(DisconnectCause.ERROR))); + } + @SmallTest @Test public void testOnFailedOutgoingCallUnholdsCallAfterLocallyDisconnect() { @@ -2986,7 +3090,6 @@ public class CallsManagerTest extends TelecomTestCase { Call call = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW); when(call.getHandoverDestinationCall()).thenReturn(destinationCall); when(call.getHandoverState()).thenReturn(HandoverState.HANDOVER_FROM_STARTED); - mCallsManager.createActionSetCallStateAndPerformAction( call, CallState.DISCONNECTED, ""); @@ -3326,7 +3429,8 @@ public class CallsManagerTest extends TelecomTestCase { false /* shouldAttachToExistingConnection*/, false /* isConference */, mClockProxy, - mToastFactory); + mToastFactory, + mFeatureFlags); ongoingCall.setState(initialState, "just cuz"); if (targetPhoneAccount == SELF_MANAGED_HANDLE || targetPhoneAccount == SELF_MANAGED_2_HANDLE) { @@ -3358,4 +3462,19 @@ public class CallsManagerTest extends TelecomTestCase { when(mockTelephonyManager.getPhoneCapability()).thenReturn(mPhoneCapability); when(mPhoneCapability.getMaxActiveVoiceSubscriptions()).thenReturn(num); } + + private void waitUntilConditionIsTrueOrTimeout(Condition condition, long timeout, + String description) throws InterruptedException { + final long start = System.currentTimeMillis(); + while (!condition.expected().equals(condition.actual()) + && System.currentTimeMillis() - start < timeout) { + sleep(50); + } + assertEquals(description, condition.expected(), condition.actual()); + } + + protected interface Condition { + Object expected(); + Object actual(); + } } diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java index cc22de291..c732720ec 100644 --- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java +++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java @@ -16,6 +16,7 @@ package com.android.server.telecom.tests; +import com.android.server.telecom.flags.FeatureFlags; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; @@ -27,6 +28,8 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import android.Manifest; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.app.AppOpsManager; import android.app.NotificationManager; import android.app.StatusBarManager; @@ -55,6 +58,7 @@ import android.location.CountryDetector; import android.location.LocationManager; import android.media.AudioDeviceInfo; import android.media.AudioManager; +import android.net.Uri; import android.os.BugreportManager; import android.os.Bundle; import android.os.DropBoxManager; @@ -81,8 +85,10 @@ import android.test.mock.MockContext; import android.util.DisplayMetrics; import android.view.accessibility.AccessibilityManager; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -409,12 +415,23 @@ public class ComponentContextFixture implements TestFixture<Context> { } @Override + public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission) { + // Override so that this can be verified via spy. + } + + @Override public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission, Bundle options) { // Override so that this can be verified via spy. } @Override + public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission, + int appOp) { + // Override so that this can be verified via spy. + } + + @Override public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, Bundle initialExtras) { @@ -617,8 +634,9 @@ public class ComponentContextFixture implements TestFixture<Context> { private TelecomManager mTelecomManager = mock(TelecomManager.class); - public ComponentContextFixture() { + public ComponentContextFixture(FeatureFlags featureFlags) { MockitoAnnotations.initMocks(this); + when(featureFlags.telecomResolveHiddenDependencies()).thenReturn(true); when(mResources.getConfiguration()).thenReturn(mResourceConfiguration); when(mResources.getString(anyInt())).thenReturn(""); when(mResources.getStringArray(anyInt())).thenReturn(new String[0]); @@ -701,7 +719,7 @@ public class ComponentContextFixture implements TestFixture<Context> { } }).when(mAppOpsManager).checkPackage(anyInt(), anyString()); - when(mNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true); + when(mNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(true); when(mCarrierConfigManager.getConfig()).thenReturn(new PersistableBundle()); when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(new PersistableBundle()); @@ -735,6 +753,14 @@ public class ComponentContextFixture implements TestFixture<Context> { mServiceInfoByComponentName.put(componentName, serviceInfo); } + public void removeConnectionService( + ComponentName componentName, + IConnectionService service) + throws Exception { + removeService(ConnectionService.SERVICE_INTERFACE, componentName, service); + mServiceInfoByComponentName.remove(componentName); + } + public void addInCallService( ComponentName componentName, IInCallService service, @@ -756,6 +782,8 @@ public class ComponentContextFixture implements TestFixture<Context> { componentName.getPackageName() }); when(mPackageManager.checkPermission(eq(Manifest.permission.CONTROL_INCALL_EXPERIENCE), eq(componentName.getPackageName()))).thenReturn(PackageManager.PERMISSION_GRANTED); + when(mPackageManager.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS), + eq(componentName.getPackageName()))).thenReturn(PackageManager.PERMISSION_GRANTED); when(mPermissionCheckerManager.checkPermission( eq(Manifest.permission.CONTROL_INCALL_EXPERIENCE), any(AttributionSourceState.class), anyString(), anyBoolean(), anyBoolean(), @@ -794,6 +822,11 @@ public class ComponentContextFixture implements TestFixture<Context> { when(mResources.getStringArray(eq(id))).thenReturn(value); } + public void putRawResource(int id, String content) { + when(mResources.openRawResource(id)) + .thenReturn(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))); + } + public void setTelecomManager(TelecomManager telecomManager) { mTelecomManager = telecomManager; } @@ -828,6 +861,12 @@ public class ComponentContextFixture implements TestFixture<Context> { mComponentNameByService.put(service, name); } + private void removeService(String action, ComponentName name, IInterface service) { + mComponentNamesByAction.remove(action, name); + mServiceByComponentName.remove(name); + mComponentNameByService.remove(service); + } + private List<ResolveInfo> doQueryIntentServices(Intent intent, int flags) { List<ResolveInfo> result = new ArrayList<>(); for (ComponentName componentName : mComponentNamesByAction.get(intent.getAction())) { diff --git a/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java b/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java index 3cb819672..c63a3d5bf 100644 --- a/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java +++ b/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java @@ -35,6 +35,7 @@ import android.content.ComponentName; import android.net.Uri; import android.os.BugreportManager; import android.os.DropBoxManager; +import android.os.UserHandle; import android.telecom.DisconnectCause; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; @@ -128,6 +129,7 @@ public class EmergencyCallDiagnosticLoggerTest extends TelecomTestCase { when(mTimeouts.getDaysBackToSearchEmergencyDiagnosticEntries()). thenReturn(DAYS_BACK_TO_SEARCH_EMERGENCY_DIAGNOSTIC_ENTRIES); when(mClockProxy.currentTimeMillis()).thenReturn(System.currentTimeMillis()); + when(mMockCallsManager.getCurrentUserHandle()).thenReturn(UserHandle.CURRENT); mEmergencyCallDiagnosticLogger = new EmergencyCallDiagnosticLogger(mTm, mBrm, mTimeouts, mDbm, Runnable::run, mClockProxy); @@ -171,7 +173,8 @@ public class EmergencyCallDiagnosticLoggerTest extends TelecomTestCase { false /* shouldAttachToExistingConnection*/, false /* isConference */, mMockClockProxy, - mMockToastProxy); + mMockToastProxy, + mFeatureFlags); } /** diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java index 683a5e205..cd8431a1a 100644 --- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java +++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java @@ -28,7 +28,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyObject; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.matches; import static org.mockito.ArgumentMatchers.nullable; @@ -36,7 +35,6 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -143,6 +141,8 @@ public class InCallControllerTests extends TelecomTestCase { @Mock PackageManager mMockPackageManager; @Mock PermissionCheckerManager mMockPermissionCheckerManager; @Mock Call mMockCall; + @Mock Call mMockSystemCall1; + @Mock Call mMockSystemCall2; @Mock Resources mMockResources; @Mock AppOpsManager mMockAppOpsManager; @Mock MockContext mMockContext; @@ -236,7 +236,7 @@ public class InCallControllerTests extends TelecomTestCase { "com.android.server.telecom.tests", null)); mInCallController = new InCallController(mMockContext, mLock, mMockCallsManager, mMockSystemStateHelper, mDefaultDialerCache, mTimeoutsAdapter, - mEmergencyCallHelper, mCarModeTracker, mClockProxy); + mEmergencyCallHelper, mCarModeTracker, mClockProxy, mFeatureFlags); // Capture the broadcast receiver registered. doAnswer(invocation -> { mRegisteredReceiver = invocation.getArgument(0); @@ -588,6 +588,7 @@ public class InCallControllerTests extends TelecomTestCase { when(mMockCall.isEmergencyCall()).thenReturn(true); when(mMockCall.isIncoming()).thenReturn(true); when(mMockCall.getAssociatedUser()).thenReturn(DUMMY_USER_HANDLE); + when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE); when(mMockContext.getSystemService(eq(UserManager.class))) .thenReturn(mMockUserManager); when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true); @@ -617,6 +618,7 @@ public class InCallControllerTests extends TelecomTestCase { when(mMockCall.isInECBM()).thenReturn(true); when(mMockCall.isIncoming()).thenReturn(true); when(mMockCall.getAssociatedUser()).thenReturn(DUMMY_USER_HANDLE); + when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE); when(mMockContext.getSystemService(eq(UserManager.class))) .thenReturn(mMockUserManager); when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true); @@ -647,6 +649,7 @@ public class InCallControllerTests extends TelecomTestCase { when(mMockCall.isInECBM()).thenReturn(true); when(mMockCall.isIncoming()).thenReturn(true); when(mMockCall.getAssociatedUser()).thenReturn(DUMMY_USER_HANDLE); + when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE); when(mMockContext.getSystemService(eq(UserManager.class))) .thenReturn(mMockUserManager); when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false); @@ -1839,6 +1842,67 @@ public class InCallControllerTests extends TelecomTestCase { assertNull(mInCallController.getInCallServiceConnections().get(testUser)); } + @Test + public void testRemoveAllServiceConnections_MultiUser() throws Exception { + when(mFeatureFlags.workProfileAssociatedUser()).thenReturn(true); + setupMocks(false /* isExternalCall */); + setupMockPackageManager(true /* default */, true /* system */, false /* external calls */); + UserHandle workUser = new UserHandle(12); + UserManager um = mContext.getSystemService(UserManager.class); + when(um.getUserInfo(anyInt())).thenReturn(mMockUserInfo); + when(mMockUserInfo.isManagedProfile()).thenReturn(false); + when(mMockCall.getAssociatedUser()).thenReturn(workUser); + setupFakeSystemCall(mMockSystemCall1, 1); + setupFakeSystemCall(mMockSystemCall2, 2); + + // Add "work" call to service. The mapping should've been inserted + // with the workUser as the key. + mInCallController.onCallAdded(mMockCall); + // Add system call to service. The mapping should've been + // inserted with the system user as the key. + mInCallController.onCallAdded(mMockSystemCall1); + + ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class); + // Make sure we bound to the system call as well as the work call. + verify(mMockContext, times(2)).bindServiceAsUser( + bindIntentCaptor.capture(), + any(ServiceConnection.class), + eq(serviceBindingFlags), + eq(UserHandle.CURRENT)); + assertTrue(mInCallController.getInCallServiceConnections().containsKey(workUser)); + assertTrue(mInCallController.getInCallServiceConnections().containsKey(UserHandle.SYSTEM)); + + // Remove the work call. This leverages getUserFromCall to remove the ICS mapping. + when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockSystemCall1)); + mInCallController.onCallRemoved(mMockCall); + waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT); + // Verify that the mapping was properly removed. + assertNull(mInCallController.getInCallServiceConnections().get(workUser)); + // Verify mapping for system user is still present. + assertNotNull(mInCallController.getInCallServiceConnections().get(UserHandle.SYSTEM)); + + // Add another system call + mInCallController.onCallAdded(mMockSystemCall2); + when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockSystemCall2)); + // Remove first system call and verify that mapping is present + mInCallController.onCallRemoved(mMockSystemCall1); + waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT); + // Verify mapping for system user is still present. + assertNotNull(mInCallController.getInCallServiceConnections().get(UserHandle.SYSTEM)); + // Remove last system call and verify that connection isn't present in ICS mapping. + when(mMockCallsManager.getCalls()).thenReturn(Collections.emptyList()); + mInCallController.onCallRemoved(mMockSystemCall2); + waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT); + assertNull(mInCallController.getInCallServiceConnections().get(UserHandle.SYSTEM)); + } + + private void setupFakeSystemCall(@Mock Call call, int id) { + when(call.getAssociatedUser()).thenReturn(UserHandle.SYSTEM); + when(call.getTargetPhoneAccount()).thenReturn(PA_HANDLE); + when(call.getAnalytics()).thenReturn(new Analytics.CallInfo()); + when(call.getId()).thenReturn("TC@" + id); + } + private void setupMocksForWorkProfileTest() { when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager); when(mMockCallsManager.isInEmergencyCall()).thenReturn(false); diff --git a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java index 88b5bb52a..39381e6ca 100644 --- a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java +++ b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java @@ -16,6 +16,7 @@ package com.android.server.telecom.tests; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telecom.IInCallAdapter; import com.android.internal.telecom.IInCallService; @@ -39,7 +40,7 @@ import java.util.concurrent.TimeUnit; * Controls a test {@link IInCallService} as would be provided by an InCall UI on a system. */ public class InCallServiceFixture implements TestFixture<IInCallService> { - + public static boolean sIgnoreOverrideAdapterFlag = false; public String mLatestCallId; public IInCallAdapter mInCallAdapter; public CallAudioState mCallAudioState; @@ -53,10 +54,17 @@ public class InCallServiceFixture implements TestFixture<IInCallService> { public CountDownLatch mUpdateCallLock = new CountDownLatch(1); public CountDownLatch mAddCallLock = new CountDownLatch(1); + @VisibleForTesting + public static void setIgnoreOverrideAdapterFlag(boolean flag) { + sIgnoreOverrideAdapterFlag = flag; + } + public class FakeInCallService extends IInCallService.Stub { @Override public void setInCallAdapter(IInCallAdapter inCallAdapter) throws RemoteException { - if (mInCallAdapter != null && inCallAdapter != null) { + // sIgnoreOverrideAdapterFlag is being used to verify a scenario where the InCallAdapter + // gets set twice (secondary user places MO/MT call). + if (mInCallAdapter != null && inCallAdapter != null && !sIgnoreOverrideAdapterFlag) { throw new RuntimeException("Adapter is already set"); } if (mInCallAdapter == null && inCallAdapter == null) { diff --git a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java index 2b05430c5..ac2f1f116 100644 --- a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java +++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java @@ -16,6 +16,8 @@ package com.android.server.telecom.tests; +import static com.android.server.telecom.ui.MissedCallNotifierImpl.CALL_LOG_COLUMN_ID; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; @@ -46,10 +48,14 @@ import android.telephony.TelephonyManager; import android.test.suitebuilder.annotation.SmallTest; import android.telecom.CallerInfo; + +import com.android.server.telecom.CallLogManager; import com.android.server.telecom.CallerInfoLookupHelper; +import com.android.server.telecom.CallsManager; import com.android.server.telecom.Constants; import com.android.server.telecom.DefaultDialerCache; import com.android.server.telecom.DeviceIdleControllerAdapter; +import com.android.server.telecom.flags.FeatureFlags; import com.android.server.telecom.MissedCallNotifier; import com.android.server.telecom.PhoneAccountRegistrar; import com.android.server.telecom.TelecomBroadcastIntentProcessor; @@ -241,7 +247,7 @@ public class MissedCallNotifierImplTest extends TelecomTestCase { MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(TEL_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP, phoneAccount.getAccountHandle()); - missedCallNotifier.showMissedCallNotification(fakeCall); + missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null); ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class); verify(mContext).sendBroadcastAsUser(intentArgumentCaptor.capture(), any(), anyString(), any()); @@ -250,6 +256,31 @@ public class MissedCallNotifierImplTest extends TelecomTestCase { assertEquals(1, sentIntent.getIntExtra(TelecomManager.EXTRA_NOTIFICATION_COUNT, -1)); } + @SmallTest + @Test + public void testCallLogUriSentToNotifier(){ + MissedCallNotifier missedCallNotifier = setupMissedCallNotificationThroughDefaultDialer(); + PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY); + Cursor mockMissedCallsCursor = new MockMissedCallCursorBuilder() + .addEntry(TEL_CALL_HANDLE.getSchemeSpecificPart(), + CallLog.Calls.PRESENTATION_ALLOWED, CALL_TIMESTAMP) + .build(); + MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(TEL_CALL_HANDLE, CALLER_NAME, + CALL_TIMESTAMP, phoneAccount.getAccountHandle()); + when(mFeatureFlags.addCallUriForMissedCalls()).thenReturn(true); + + missedCallNotifier.showMissedCallNotification(fakeCall, + CallLog.Calls.CONTENT_URI.buildUpon().appendPath(Long.toString( + mockMissedCallsCursor.getInt(CALL_LOG_COLUMN_ID))).build()); + ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mContext).sendBroadcastAsUser(intentArgumentCaptor.capture(), any(), + anyString(), any()); + + Intent sentIntent = intentArgumentCaptor.getValue(); + Uri actualCallUri = sentIntent.getParcelableExtra(TelecomManager.EXTRA_CALL_LOG_URI); + assertTrue(actualCallUri.isPathPrefixMatch(CallLog.Calls.CONTENT_URI)); + } + private MissedCallNotifier setupMissedCallNotificationThroughDefaultDialer() { mComponentContextFixture.addIntentReceiver( TelecomManager.ACTION_SHOW_MISSED_CALLS_NOTIFICATION, COMPONENT_NAME); @@ -275,9 +306,9 @@ public class MissedCallNotifierImplTest extends TelecomTestCase { MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(TEL_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP, phoneAccount.getAccountHandle()); - missedCallNotifier.showMissedCallNotification(fakeCall); + missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */null); missedCallNotifier.clearMissedCalls(userHandle); - missedCallNotifier.showMissedCallNotification(fakeCall); + missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */null); ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass( Integer.class); @@ -308,10 +339,10 @@ public class MissedCallNotifierImplTest extends TelecomTestCase { MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext, mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory, - mDeviceIdleControllerAdapter); + mDeviceIdleControllerAdapter, mFeatureFlags); - missedCallNotifier.showMissedCallNotification(fakeCall); - missedCallNotifier.showMissedCallNotification(fakeCall); + missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null); + missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null); // The following captor is to capture the two notifications that got passed into // notifyAsUser. This distinguishes between the builders used for the full notification @@ -402,7 +433,7 @@ public class MissedCallNotifierImplTest extends TelecomTestCase { MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(TEL_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP, phoneAccount.getAccountHandle()); - missedCallNotifier.showMissedCallNotification(fakeCall); + missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null); ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass( Notification.class); @@ -464,13 +495,13 @@ public class MissedCallNotifierImplTest extends TelecomTestCase { MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext, mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory, - mDeviceIdleControllerAdapter); + mDeviceIdleControllerAdapter, mFeatureFlags); PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY); MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(SIP_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP, phoneAccount.getAccountHandle()); - missedCallNotifier.showMissedCallNotification(fakeCall); + missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null); // Create two intents that correspond to call-back and respond back with SMS, and assert // that in the case of a SIP call, no SMS intent is generated. @@ -525,7 +556,7 @@ public class MissedCallNotifierImplTest extends TelecomTestCase { MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext, mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory, - mDeviceIdleControllerAdapter); + mDeviceIdleControllerAdapter, mFeatureFlags); // AsyncQueryHandler used in reloadFromDatabase interacts poorly with the below // timeout-verify, so run this in a new handler to mitigate that. @@ -595,7 +626,7 @@ public class MissedCallNotifierImplTest extends TelecomTestCase { MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext, mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory, - mDeviceIdleControllerAdapter); + mDeviceIdleControllerAdapter, mFeatureFlags); // AsyncQueryHandler used in reloadFromDatabase interacts poorly with the below // timeout-verify, so run this in a new handler to mitigate that. @@ -637,13 +668,13 @@ public class MissedCallNotifierImplTest extends TelecomTestCase { MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext, mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory, - mDeviceIdleControllerAdapter); + mDeviceIdleControllerAdapter, mFeatureFlags); PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY); MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(SIP_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP, phoneAccount.getAccountHandle()); - missedCallNotifier.showMissedCallNotification(fakeCall); + missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null); ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); ArgumentCaptor<Bundle> bundleCaptor = @@ -701,7 +732,7 @@ public class MissedCallNotifierImplTest extends TelecomTestCase { NotificationBuilderFactory fakeBuilderFactory, UserHandle currentUser) { MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext, mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory, - mDeviceIdleControllerAdapter); + mDeviceIdleControllerAdapter, mFeatureFlags); missedCallNotifier.setCurrentUserHandle(currentUser); return missedCallNotifier; } diff --git a/tests/src/com/android/server/telecom/tests/MissedInformationTest.java b/tests/src/com/android/server/telecom/tests/MissedInformationTest.java index 4af3de343..f28966edd 100644 --- a/tests/src/com/android/server/telecom/tests/MissedInformationTest.java +++ b/tests/src/com/android/server/telecom/tests/MissedInformationTest.java @@ -109,6 +109,7 @@ public class MissedInformationTest extends TelecomSystemTest { mAdapter = new CallIntentProcessor.AdapterImpl(mCallsManager.getDefaultDialerCache()); mNotificationManager = spy((NotificationManager) mContext.getSystemService( Context.NOTIFICATION_SERVICE)); + when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true); when(mContentResolver.getPackageName()).thenReturn(PACKAGE_NAME); when(mContentResolver.acquireProvider(any(String.class))).thenReturn(mContentProvider); when(mContentProvider.call(any(String.class), any(String.class), @@ -152,6 +153,8 @@ public class MissedInformationTest extends TelecomSystemTest { setUpEmergencyCall(); when(mEmergencyCall.getAssociatedUser()). thenReturn(mPhoneAccountA0.getAccountHandle().getUserHandle()); + when(mEmergencyCall.getTargetPhoneAccount()) + .thenReturn(mPhoneAccountA0.getAccountHandle()); mCallsManager.addCall(mEmergencyCall); assertTrue(mCallsManager.isInEmergencyCall()); @@ -358,7 +361,7 @@ public class MissedInformationTest extends TelecomSystemTest { setUpIncomingCall(); doReturn(mNotificationManager).when(mSpyContext) .getSystemService(Context.NOTIFICATION_SERVICE); - doReturn(false).when(mNotificationManager).matchesCallFilter(any(Bundle.class)); + doReturn(false).when(mNotificationManager).matchesCallFilter(any(Uri.class)); doReturn(false).when(mIncomingCall).wasDndCheckComputedForCall(); mCallsManager.getRinger().setNotificationManager(mNotificationManager); @@ -369,7 +372,7 @@ public class MissedInformationTest extends TelecomSystemTest { // Wait for ringer attributes build completed verify(mNotificationManager, timeout(TEST_TIMEOUT_MILLIS)) - .matchesCallFilter(any(Bundle.class)); + .matchesCallFilter(any(Uri.class)); mCallsManager.getRinger().waitForAttributesCompletion(); mCallsManager.markCallAsDisconnected(mIncomingCall, @@ -417,7 +420,7 @@ public class MissedInformationTest extends TelecomSystemTest { null, mCallsManager.getPhoneNumberUtilsAdapter(), null, null, null, mPhoneAccountA0.getAccountHandle(), Call.CALL_DIRECTION_INCOMING, false, false, - mClockProxy, null)); + mClockProxy, null, mFeatureFlags)); doReturn(1L).when(mIncomingCall).getStartRingTime(); doAnswer((x) -> { mCountDownLatch.countDown(); diff --git a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java index 33acd9811..1ffcb7664 100644 --- a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java +++ b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java @@ -53,6 +53,7 @@ import android.telephony.DisconnectCause; import android.telephony.TelephonyManager; import android.test.suitebuilder.annotation.SmallTest; +import com.android.server.telecom.flags.FeatureFlags; import com.android.server.telecom.Call; import com.android.server.telecom.CallsManager; import com.android.server.telecom.DefaultDialerCache; @@ -75,6 +76,8 @@ import org.mockito.Mock; @RunWith(JUnit4.class) public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase { + private static final Uri TEST_URI = Uri.parse("tel:16505551212"); + private static class ReceiverIntentPair { public BroadcastReceiver receiver; public Intent intent; @@ -93,6 +96,7 @@ public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase { @Mock private PhoneAccountRegistrar mPhoneAccountRegistrar; @Mock private RoleManagerAdapter mRoleManagerAdapter; @Mock private DefaultDialerCache mDefaultDialerCache; + @Mock private FeatureFlags mFeatureFlags; @Mock private MmiUtils mMmiUtils; private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter = new PhoneNumberUtilsAdapterImpl(); @@ -113,6 +117,7 @@ public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase { any(PhoneAccountHandle.class))).thenReturn(mPhoneAccount); when(mPhoneAccount.isSelfManaged()).thenReturn(true); when(mSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(false); + when(mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()).thenReturn(false); } @Override @@ -510,6 +515,84 @@ public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase { testUnmodifiedRegularCall(); } + /** + * Where the flag `isNewOutgoingCallBroadcastUnblocking` is off, verify that we sent an ordered + * broadcast and did not try to start the call immediately (legacy behavior). + */ + @SmallTest + @Test + public void testSendBroadcastBlocking() { + when(mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()).thenReturn(false); + Intent intent = new Intent(Intent.ACTION_CALL, TEST_URI); + NewOutgoingCallIntentBroadcaster nocib = new NewOutgoingCallIntentBroadcaster( + mContext, mCallsManager, intent, mPhoneNumberUtilsAdapter, + true /* isDefaultPhoneApp */, mDefaultDialerCache, mMmiUtils, mFeatureFlags); + + NewOutgoingCallIntentBroadcaster.CallDisposition disposition = nocib.evaluateCall(); + nocib.processCall(mCall, disposition); + + // We should not have not short-circuited to place the outgoing call directly. + verify(mCall, never()).setNewOutgoingCallIntentBroadcastIsDone(); + verify(mCallsManager, never()).placeOutgoingCall(any(Call.class), any(Uri.class), + any(GatewayInfo.class), anyBoolean(), anyInt()); + + // Ensure we did send the broadcast ordered + verifyBroadcastSent(TEST_URI.getSchemeSpecificPart(), + createNumberExtras(TEST_URI.getSchemeSpecificPart())); + + // Ensure we did not try to directly send the broadcast unordered. + verify(mContext, never()).sendBroadcastAsUser( + any(Intent.class), + eq(UserHandle.CURRENT), + eq(android.Manifest.permission.PROCESS_OUTGOING_CALLS)); + } + + /** + * Where the flag `isNewOutgoingCallBroadcastUnblocking` is off, verify that we sent an ordered + * broadcast and did not try to start the call immediately. Also ensure that the broadcast + * flags are correct. + */ + @SmallTest + @Test + public void testSendBroadcastNonBlocking() { + when(mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()).thenReturn(true); + Intent intent = new Intent(Intent.ACTION_CALL, TEST_URI); + NewOutgoingCallIntentBroadcaster nocib = new NewOutgoingCallIntentBroadcaster( + mContext, mCallsManager, intent, mPhoneNumberUtilsAdapter, + true /* isDefaultPhoneApp */, mDefaultDialerCache, mMmiUtils, mFeatureFlags); + + NewOutgoingCallIntentBroadcaster.CallDisposition disposition = nocib.evaluateCall(); + nocib.processCall(mCall, disposition); + + // We should have started the outgoing call flow immediately. + verify(mCall).setNewOutgoingCallIntentBroadcastIsDone(); + verify(mCallsManager).placeOutgoingCall(any(Call.class), any(Uri.class), + nullable(GatewayInfo.class), anyBoolean(), anyInt()); + + // Ensure we didn't send an ordered broadcast. + verify(mContext, never()).sendOrderedBroadcastAsUser( + any(Intent.class), + any(UserHandle.class), + anyString(), + anyInt(), + any(Bundle.class), + any(BroadcastReceiver.class), + any(Handler.class), + eq(Activity.RESULT_OK), + anyString(), + any(Bundle.class)); + + // But that we did send a regular broadcast. + ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mContext).sendBroadcastAsUser( + intentArgumentCaptor.capture(), + eq(UserHandle.CURRENT), + eq(android.Manifest.permission.PROCESS_OUTGOING_CALLS), + eq(AppOpsManager.OP_PROCESS_OUTGOING_CALLS)); + Intent capturedIntent = intentArgumentCaptor.getValue(); + assertEquals(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, capturedIntent.getFlags()); + } + private ReceiverIntentPair regularCallTestHelper(Intent intent, Bundle expectedAdditionalExtras) { Uri handle = intent.getData(); @@ -542,7 +625,7 @@ public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase { boolean isDefaultPhoneApp) { NewOutgoingCallIntentBroadcaster b = new NewOutgoingCallIntentBroadcaster( mContext, mCallsManager, intent, mPhoneNumberUtilsAdapter, - isDefaultPhoneApp, mDefaultDialerCache, mMmiUtils); + isDefaultPhoneApp, mDefaultDialerCache, mMmiUtils, mFeatureFlags); NewOutgoingCallIntentBroadcaster.CallDisposition cd = b.evaluateCall(); if (cd.disconnectCause == DisconnectCause.NOT_DISCONNECTED) { b.processCall(mCall, cd); diff --git a/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java b/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java index fed80847f..57c61915b 100644 --- a/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java +++ b/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java @@ -13,6 +13,7 @@ import android.content.ComponentName; import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; +import android.os.UserHandle; import android.telecom.Connection; import android.telecom.ParcelableCall; import android.telecom.PhoneAccountHandle; @@ -57,6 +58,7 @@ public class ParcelableCallUtilsTest extends TelecomTestCase { when(mClockProxy.elapsedRealtime()).thenReturn(SystemClock.elapsedRealtime()); when(mCallsManager.getCallerInfoLookupHelper()).thenReturn(mCallerInfoLookupHelper); when(mCallsManager.getPhoneAccountRegistrar()).thenReturn(mPhoneAccountRegistrar); + when(mCallsManager.getCurrentUserHandle()).thenReturn(UserHandle.CURRENT); when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(any())).thenReturn(null); when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(any())) .thenReturn(false); @@ -75,7 +77,8 @@ public class ParcelableCallUtilsTest extends TelecomTestCase { false /* shouldAttachToExistingConnection */, false /* isConference */, mClockProxy /* ClockProxy */, - mToastProxy); + mToastProxy, + mFeatureFlags); } @Override diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java index e573bb81f..9fcb87a38 100644 --- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java +++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java @@ -353,6 +353,40 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase { PhoneAccount.SCHEME_TEL)); } + /** + * Verify when a {@link android.telecom.ConnectionService} is disabled or cannot be resolved, + * all phone accounts are unregistered when calling + * {@link PhoneAccountRegistrar#getAccountsForPackage_BypassResolveComp(String, UserHandle)}. + */ + @Test + public void testCannotResolveServiceUnregistersAccounts() throws Exception { + ComponentName componentName = makeQuickConnectionServiceComponentName(); + PhoneAccount account = makeQuickAccountBuilder("0", 0, USER_HANDLE_10) + .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER + | PhoneAccount.CAPABILITY_CALL_PROVIDER).build(); + // add the ConnectionService and register a single phone account for it + mComponentContextFixture.addConnectionService(componentName, + Mockito.mock(IConnectionService.class)); + registerAndEnableAccount(account); + // verify the start state + assertEquals(1, + mRegistrar.getAccountsForPackage_BypassResolveComp(componentName.getPackageName(), + USER_HANDLE_10).size()); + // remove the ConnectionService so that the account cannot be resolved anymore + mComponentContextFixture.removeConnectionService(componentName, + Mockito.mock(IConnectionService.class)); + // verify the account is unregistered when fetching the phone accounts for the package + assertEquals(1, + mRegistrar.getAccountsForPackage_BypassResolveComp(componentName.getPackageName(), + USER_HANDLE_10).size()); + assertEquals(0,mRegistrar.cleanupUnresolvableConnectionServiceAccounts( + mRegistrar.getAccountsForPackage_BypassResolveComp(componentName.getPackageName(), + USER_HANDLE_10)).size()); + assertEquals(0, + mRegistrar.getAccountsForPackage_BypassResolveComp(componentName.getPackageName(), + USER_HANDLE_10).size()); + } + @MediumTest @Test public void testSimCallManager() throws Exception { diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java index 34360cadc..985438409 100644 --- a/tests/src/com/android/server/telecom/tests/RingerTest.java +++ b/tests/src/com/android/server/telecom/tests/RingerTest.java @@ -16,8 +16,9 @@ package com.android.server.telecom.tests; +import static android.os.VibrationEffect.EFFECT_CLICK; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; - +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -30,6 +31,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -51,6 +53,9 @@ import android.os.UserManager; import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; +import android.os.VibratorInfo; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; import android.test.suitebuilder.annotation.SmallTest; @@ -63,33 +68,47 @@ import com.android.server.telecom.InCallTonePlayer; import com.android.server.telecom.Ringer; import com.android.server.telecom.RingtoneFactory; import com.android.server.telecom.SystemSettingsUtil; +import com.android.server.telecom.flags.FeatureFlags; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mock; import org.mockito.Spy; +import java.time.Duration; import java.util.concurrent.CompletableFuture; @RunWith(JUnit4.class) public class RingerTest extends TelecomTestCase { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final Uri FAKE_RINGTONE_URI = Uri.parse("content://media/fake/audio/1729"); // Returned when the a URI-based VibrationEffect is attempted, to avoid depending on actual // device configuration for ringtone URIs. The actual Uri can be verified via the // VibrationEffectProxy mock invocation. private static final VibrationEffect URI_VIBRATION_EFFECT = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK); + private static final VibrationEffect EXPECTED_SIMPLE_VIBRATION_PATTERN = + VibrationEffect.createWaveform( + new long[] {0, 1000, 1000}, new int[] {0, 255, 0}, 1); + private static final VibrationEffect EXPECTED_PULSE_VIBRATION_PATTERN = + VibrationEffect.createWaveform( + Ringer.PULSE_PATTERN, Ringer.PULSE_AMPLITUDE, 5); @Mock InCallTonePlayer.Factory mockPlayerFactory; @Mock SystemSettingsUtil mockSystemSettingsUtil; @Mock RingtoneFactory mockRingtoneFactory; @Mock Vibrator mockVibrator; + @Mock VibratorInfo mockVibratorInfo; @Mock InCallController mockInCallController; @Mock NotificationManager mockNotificationManager; @Mock Ringer.AccessibilityManagerAdapter mockAccessibilityManagerAdapter; + @Mock private FeatureFlags mFeatureFlags; @Spy Ringer.VibrationEffectProxy spyVibrationEffectProxy; @@ -111,21 +130,25 @@ public class RingerTest extends TelecomTestCase { @Before public void setUp() throws Exception { super.setUp(); - mContext = mComponentContextFixture.getTestDouble().getApplicationContext(); + mContext = spy(mComponentContextFixture.getTestDouble().getApplicationContext()); + when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true); doReturn(URI_VIBRATION_EFFECT).when(spyVibrationEffectProxy).get(any(), any()); when(mockPlayerFactory.createPlayer(anyInt())).thenReturn(mockTonePlayer); mockAudioManager = mContext.getSystemService(AudioManager.class); when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_NORMAL); + when(mockVibrator.getInfo()).thenReturn(mockVibratorInfo); when(mockSystemSettingsUtil.isHapticPlaybackSupported(any(Context.class))) .thenAnswer((invocation) -> mIsHapticPlaybackSupported); mockNotificationManager =mContext.getSystemService(NotificationManager.class); when(mockTonePlayer.startTone()).thenReturn(true); - when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true); + when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(true); when(mockRingtoneFactory.hasHapticChannels(any(Ringtone.class))).thenReturn(false); when(mockCall1.getState()).thenReturn(CallState.RINGING); when(mockCall2.getState()).thenReturn(CallState.RINGING); when(mockCall1.getAssociatedUser()).thenReturn(PA_HANDLE.getUserHandle()); when(mockCall2.getAssociatedUser()).thenReturn(PA_HANDLE.getUserHandle()); + when(mockCall1.getTargetPhoneAccount()).thenReturn(PA_HANDLE); + when(mockCall2.getTargetPhoneAccount()).thenReturn(PA_HANDLE); // Set BT active state in tests to ensure that we do not end up blocking tests for 1 sec // waiting for BT to connect in unit tests by default. asyncRingtonePlayer.updateBtActiveState(true); @@ -140,7 +163,8 @@ public class RingerTest extends TelecomTestCase { private void createRingerUnderTest() { mRingerUnderTest = new Ringer(mockPlayerFactory, mContext, mockSystemSettingsUtil, asyncRingtonePlayer, mockRingtoneFactory, mockVibrator, spyVibrationEffectProxy, - mockInCallController, mockNotificationManager, mockAccessibilityManagerAdapter); + mockInCallController, mockNotificationManager, mockAccessibilityManagerAdapter, + mFeatureFlags); // This future is used to wait for AsyncRingtonePlayer to finish its part. mRingerUnderTest.setBlockOnRingingFuture(mRingCompletionFuture); } @@ -153,6 +177,151 @@ public class RingerTest extends TelecomTestCase { @SmallTest @Test + public void testSimpleVibrationPrecedesValidSupportedDefaultRingVibrationOverride() + throws Exception { + when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true); + mockVibrationResourceValues( + """ + <vibration> + <predefined-effect name="click"/> + </vibration> + """, + /* useSimpleVibration= */ true); + when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true); + + createRingerUnderTest(); + + assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect); + } + + @SmallTest + @Test + public void testDefaultRingVibrationOverrideNotUsedWhenFeatureIsDisabled() + throws Exception { + when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(false); + mockVibrationResourceValues( + """ + <vibration> + <waveform-effect> + <waveform-entry durationMs="100" amplitude="0"/> + <repeating> + <waveform-entry durationMs="500" amplitude="default"/> + <waveform-entry durationMs="700" amplitude="0"/> + </repeating> + </waveform-effect> + </vibration> + """, + /* useSimpleVibration= */ false); + when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true); + + createRingerUnderTest(); + + assertEquals(EXPECTED_PULSE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect); + } + + @SmallTest + @Test + public void testValidSupportedRepeatingDefaultRingVibrationOverride() throws Exception { + when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true); + mockVibrationResourceValues( + """ + <vibration> + <waveform-effect> + <waveform-entry durationMs="100" amplitude="0"/> + <repeating> + <waveform-entry durationMs="500" amplitude="default"/> + <waveform-entry durationMs="700" amplitude="0"/> + </repeating> + </waveform-effect> + </vibration> + """, + /* useSimpleVibration= */ false); + when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true); + + createRingerUnderTest(); + + assertEquals( + VibrationEffect.createWaveform(new long[]{100, 500, 700}, /* repeat= */ 1), + mRingerUnderTest.mDefaultVibrationEffect); + } + + @SmallTest + @Test + public void testValidSupportedNonRepeatingDefaultRingVibrationOverride() throws Exception { + when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true); + mockVibrationResourceValues( + """ + <vibration> + <predefined-effect name="click"/> + </vibration> + """, + /* useSimpleVibration= */ false); + when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true); + + createRingerUnderTest(); + + assertEquals( + VibrationEffect + .startComposition() + .repeatEffectIndefinitely( + VibrationEffect + .startComposition() + .addEffect(VibrationEffect.createPredefined(EFFECT_CLICK)) + .addOffDuration(Duration.ofSeconds(1)) + .compose() + ) + .compose(), + mRingerUnderTest.mDefaultVibrationEffect); + } + + @SmallTest + @Test + public void testValidButUnsupportedDefaultRingVibrationOverride() throws Exception { + when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true); + mockVibrationResourceValues( + """ + <vibration> + <predefined-effect name="click"/> + </vibration> + """, + /* useSimpleVibration= */ false); + when(mockVibratorInfo.areVibrationFeaturesSupported( + eq(VibrationEffect.createPredefined(EFFECT_CLICK)))).thenReturn(false); + + createRingerUnderTest(); + + assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect); + } + + @SmallTest + @Test + public void testInvalidDefaultRingVibrationOverride() throws Exception { + when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true); + mockVibrationResourceValues( + /* defaultVibrationContent= */ "bad serialization", + /* useSimpleVibration= */ false); + when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true); + + createRingerUnderTest(); + + assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect); + } + + @SmallTest + @Test + public void testEmptyDefaultRingVibrationOverride() throws Exception { + when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true); + mockVibrationResourceValues( + /* defaultVibrationContent= */ "", /* useSimpleVibration= */ false); + when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true); + + createRingerUnderTest(); + + assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect); + } + + @SmallTest + @Test public void testNoActionInTheaterMode() throws Exception { // Start call waiting to make sure that it doesn't stop when we start ringing mRingerUnderTest.startCallWaiting(mockCall1); @@ -231,7 +400,7 @@ public class RingerTest extends TelecomTestCase { @SmallTest @Test public void testCallWaitingButNoRingForSpecificContacts() throws Exception { - when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false); + when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(false); // Start call waiting to make sure that it does stop when we start ringing mRingerUnderTest.startCallWaiting(mockCall1); verify(mockTonePlayer).startTone(); @@ -492,7 +661,7 @@ public class RingerTest extends TelecomTestCase { when(mContext.getSystemService(NotificationManager.class)).thenReturn( mockNotificationManager); // suppress the call - when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false); + when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(false); // run the method under test assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1)); @@ -501,7 +670,7 @@ public class RingerTest extends TelecomTestCase { // verify we never set the call object and matchesCallFilter is called verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(true); verify(mockNotificationManager, times(1)) - .matchesCallFilter(any(Bundle.class)); + .matchesCallFilter(any(Uri.class)); } /** @@ -514,7 +683,6 @@ public class RingerTest extends TelecomTestCase { when(mockCall1.wasDndCheckComputedForCall()).thenReturn(false); when(mockCall1.getHandle()).thenReturn(Uri.parse("")); // alert the user of the call - when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true); // run the method under test assertTrue(mRingerUnderTest.shouldRingForContact(mockCall1)); @@ -523,7 +691,7 @@ public class RingerTest extends TelecomTestCase { // verify we never set the call object and matchesCallFilter is called verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false); verify(mockNotificationManager, times(1)) - .matchesCallFilter(any(Bundle.class)); + .matchesCallFilter(any(Uri.class)); } /** @@ -539,7 +707,7 @@ public class RingerTest extends TelecomTestCase { // THEN assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1)); verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false); - verify(mockNotificationManager, never()).matchesCallFilter(any(Bundle.class)); + verify(mockNotificationManager, never()).matchesCallFilter(any(Uri.class)); } @Test @@ -549,7 +717,7 @@ public class RingerTest extends TelecomTestCase { mRingerUnderTest.startCallWaiting(mockCall1); when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false); when(mockCall2.getHandle()).thenReturn(Uri.parse("")); - when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false); + when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(false); assertFalse(mRingerUnderTest.shouldRingForContact(mockCall2)); assertFalse(startRingingAndWaitForAsync(mockCall2, false)); @@ -564,7 +732,6 @@ public class RingerTest extends TelecomTestCase { mRingerUnderTest.startCallWaiting(mockCall1); when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false); when(mockCall2.getHandle()).thenReturn(Uri.parse("")); - when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true); assertTrue(mRingerUnderTest.shouldRingForContact(mockCall2)); assertTrue(startRingingAndWaitForAsync(mockCall2, false)); @@ -591,7 +758,6 @@ public class RingerTest extends TelecomTestCase { mRingerUnderTest.startCallWaiting(mockCall1); when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false); when(mockCall2.getHandle()).thenReturn(Uri.parse("")); - when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true); assertTrue(mRingerUnderTest.shouldRingForContact(mockCall2)); assertTrue(mRingerUnderTest.startRinging(mockCall2, false)); @@ -670,4 +836,13 @@ public class RingerTest extends TelecomTestCase { when(mockRingtoneFactory.getHapticOnlyRingtone()).thenReturn(mockRingtone); return mockRingtone; } + + private void mockVibrationResourceValues( + String defaultVibrationContent, boolean useSimpleVibration) { + mComponentContextFixture.putRawResource( + com.android.internal.R.raw.default_ringtone_vibration_effect, + defaultVibrationContent); + mComponentContextFixture.putBooleanResource( + R.bool.use_simple_vibration_pattern, useSimpleVibration); + } } diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java index 8bc1f2a4d..e9466ee23 100644 --- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java +++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java @@ -58,11 +58,13 @@ import com.android.server.telecom.CallIntentProcessor; import com.android.server.telecom.CallState; import com.android.server.telecom.CallsManager; import com.android.server.telecom.DefaultDialerCache; +import com.android.server.telecom.InCallController; import com.android.server.telecom.PhoneAccountRegistrar; import com.android.server.telecom.TelecomServiceImpl; import com.android.server.telecom.TelecomSystem; import com.android.server.telecom.components.UserCallIntentProcessor; import com.android.server.telecom.components.UserCallIntentProcessorFactory; +import com.android.server.telecom.flags.FeatureFlags; import com.android.server.telecom.voip.IncomingCallTransaction; import com.android.server.telecom.voip.OutgoingCallTransaction; import com.android.server.telecom.voip.TransactionManager; @@ -76,6 +78,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; import org.mockito.Mock; +import java.lang.reflect.Method; import java.util.Collection; import java.util.List; import java.util.concurrent.Executor; @@ -86,6 +89,7 @@ import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION; import static android.Manifest.permission.WRITE_SECURE_SETTINGS; 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.assertThrows; import static org.junit.Assert.assertTrue; @@ -122,7 +126,7 @@ public class TelecomServiceImplTest extends TelecomTestCase { public static class CallIntentProcessAdapterFake implements CallIntentProcessor.Adapter { @Override public void processOutgoingCallIntent(Context context, CallsManager callsManager, - Intent intent, String callingPackage) { + Intent intent, String callingPackage, FeatureFlags flags) { } @@ -192,6 +196,9 @@ public class TelecomServiceImplTest extends TelecomTestCase { @Mock private ICallEventCallback mICallEventCallback; @Mock private TransactionManager mTransactionManager; @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter; + @Mock private FeatureFlags mFeatureFlags; + + @Mock private InCallController mInCallController; private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { }; @@ -219,6 +226,7 @@ public class TelecomServiceImplTest extends TelecomTestCase { doReturn(mContext).when(mContext).getApplicationContext(); doReturn(mContext).when(mContext).createContextAsUser(any(UserHandle.class), anyInt()); + when(mFakeCallsManager.getInCallController()).thenReturn(mInCallController); doNothing().when(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class), anyString()); when(mContext.checkCallingOrSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS)) @@ -242,6 +250,7 @@ public class TelecomServiceImplTest extends TelecomTestCase { mDefaultDialerCache, mSubscriptionManagerAdapter, mSettingsSecureAdapter, + mFeatureFlags, mLock); telecomServiceImpl.setTransactionManager(mTransactionManager); telecomServiceImpl.setAnomalyReporterAdapter(mAnomalyReporterAdapter); @@ -260,6 +269,7 @@ public class TelecomServiceImplTest extends TelecomTestCase { mPackageManager = mContext.getPackageManager(); when(mPackageManager.getPackageUid(anyString(), eq(0))).thenReturn(Binder.getCallingUid()); + when(mFeatureFlags.earlyBindingToIncallService()).thenReturn(true); } @Override @@ -1040,6 +1050,7 @@ public class TelecomServiceImplTest extends TelecomTestCase { verify(mFakePhoneAccountRegistrar).getPhoneAccount( TEL_PA_HANDLE_16, TEL_PA_HANDLE_16.getUserHandle()); + verify(mInCallController, never()).bindToServices(any()); addCallTestHelper(TelecomManager.ACTION_INCOMING_CALL, CallIntentProcessor.KEY_IS_INCOMING_CALL, extras, TEL_PA_HANDLE_16, false); @@ -1047,6 +1058,81 @@ public class TelecomServiceImplTest extends TelecomTestCase { @SmallTest @Test + public void testAddNewIncomingFlagDisabledNoEarlyBinding() throws Exception { + when(mFeatureFlags.earlyBindingToIncallService()).thenReturn(false); + PhoneAccount phoneAccount = makeSkipCallFilteringPhoneAccount(TEL_PA_HANDLE_16).build(); + phoneAccount.setIsEnabled(true); + doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount( + eq(TEL_PA_HANDLE_16), any(UserHandle.class)); + doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked( + eq(TEL_PA_HANDLE_16)); + doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString()); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true); + Bundle extras = createSampleExtras(); + + mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE); + + verify(mInCallController, never()).bindToServices(null); + } + + @SmallTest + @Test + public void testAddNewIncomingCallEarlyBindingForNoCallFilterCalls() throws Exception { + PhoneAccount phoneAccount = makeSkipCallFilteringPhoneAccount(TEL_PA_HANDLE_16).build(); + phoneAccount.setIsEnabled(true); + doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount( + eq(TEL_PA_HANDLE_16), any(UserHandle.class)); + doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked( + eq(TEL_PA_HANDLE_16)); + doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString()); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true); + Bundle extras = createSampleExtras(); + + mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE); + + verify(mInCallController).bindToServices(null); + } + + @SmallTest + @Test + public void testAddNewIncomingCallEarlyBindingNotEnableForNonWatchDevices() throws Exception { + PhoneAccount phoneAccount = makeSkipCallFilteringPhoneAccount(TEL_PA_HANDLE_16).build(); + phoneAccount.setIsEnabled(true); + doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount( + eq(TEL_PA_HANDLE_16), any(UserHandle.class)); + doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked( + eq(TEL_PA_HANDLE_16)); + doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString()); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false); + Bundle extras = createSampleExtras(); + + mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE); + + verify(mInCallController, never()).bindToServices(null); + } + + @SmallTest + @Test + public void testAddNewIncomingCallEarlyBindingNotEnableForPhoneAccountHasCallFilters() + throws Exception { + PhoneAccount phoneAccount = makePhoneAccount(TEL_PA_HANDLE_16).build(); + phoneAccount.setIsEnabled(true); + doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount( + eq(TEL_PA_HANDLE_16), any(UserHandle.class)); + doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked( + eq(TEL_PA_HANDLE_16)); + doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString()); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true); + Bundle extras = createSampleExtras(); + + mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE); + + verify(mInCallController, never()).bindToServices(null); + } + + + @SmallTest + @Test public void testAddNewIncomingCallFailure() throws Exception { try { mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, null, CALLING_PACKAGE); @@ -1703,6 +1789,28 @@ public class TelecomServiceImplTest extends TelecomTestCase { verify(mContext, never()).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class)); } + /** + * FeatureFlags is autogenerated code, so there could be a situation where something changes + * outside of Telecom control that breaks reflection. This test attempts to ensure that changes + * to auto-generated FeatureFlags code that breaks reflection are caught early. + */ + @SmallTest + @Test + public void testFlagConfigReflectionWorks() { + try { + Method[] methods = FeatureFlags.class.getMethods(); + for (Method m : methods) { + // test getting the name and invoking the flag code + String name = m.getName(); + Object val = m.invoke(mFeatureFlags); + assertNotNull(name); + assertNotNull(val); + } + } catch (Exception e) { + fail("Reflection failed for FeatureFlags with error: " + e); + } + } + @SmallTest @Test public void testIsVoicemailNumber() throws Exception { @@ -2144,6 +2252,12 @@ public class TelecomServiceImplTest extends TelecomTestCase { return new PhoneAccount.Builder(paHandle, "testLabel"); } + private PhoneAccount.Builder makeSkipCallFilteringPhoneAccount(PhoneAccountHandle paHandle) { + Bundle extras = new Bundle(); + extras.putBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING, true); + return new PhoneAccount.Builder(paHandle, "testLabel").setExtras(extras); + } + private Bundle createSampleExtras() { Bundle extras = new Bundle(); extras.putString("test_key", "test_value"); diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java index ed96d7438..aa2cf5662 100644 --- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java +++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java @@ -38,6 +38,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.annotation.Nullable; import android.app.AppOpsManager; import android.bluetooth.BluetoothManager; import android.content.BroadcastReceiver; @@ -99,6 +100,7 @@ import com.android.server.telecom.WiredHeadsetManager; import com.android.server.telecom.bluetooth.BluetoothRouteManager; import com.android.server.telecom.callfiltering.BlockedNumbersAdapter; import com.android.server.telecom.components.UserCallIntentProcessor; +import com.android.server.telecom.flags.FeatureFlags; import com.android.server.telecom.ui.IncomingCallNotifier; import com.google.common.base.Predicate; @@ -120,7 +122,7 @@ import java.util.concurrent.TimeUnit; /** * Implements mocks and functionality required to implement telecom system tests. */ -public class TelecomSystemTest extends TelecomTestCase { +public class TelecomSystemTest extends TelecomTestCase{ private static final String CALLING_PACKAGE = TelecomSystemTest.class.getPackageName(); static final int TEST_POLL_INTERVAL = 10; // milliseconds @@ -168,7 +170,7 @@ public class TelecomSystemTest extends TelecomTestCase { } @Override - public void showMissedCallNotification(CallInfo call) { + public void showMissedCallNotification(CallInfo call, @Nullable Uri uri) { missedCallsNotified.add(call); } @@ -217,6 +219,8 @@ public class TelecomSystemTest extends TelecomTestCase { BlockedNumbersAdapter mBlockedNumbersAdapter; @Mock CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker; + @Mock + FeatureFlags mFeatureFlags; final ComponentName mInCallServiceComponentNameX = new ComponentName( @@ -323,6 +327,20 @@ public class TelecomSystemTest extends TelecomTestCase { PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) .build(); + final PhoneAccount mPhoneAccountMultiUser = + PhoneAccount.builder( + new PhoneAccountHandle( + mConnectionServiceComponentNameA, + "id MU", UserHandle.of(12)), + "Phone account service MU") + .addSupportedUriScheme("tel") + .setCapabilities( + PhoneAccount.CAPABILITY_CALL_PROVIDER | + PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION | + PhoneAccount.CAPABILITY_VIDEO_CALLING | + PhoneAccount.CAPABILITY_MULTI_USER) + .build(); + ConnectionServiceFixture mConnectionServiceFixtureA; ConnectionServiceFixture mConnectionServiceFixtureB; Timeouts.Adapter mTimeoutsAdapter; @@ -496,9 +514,11 @@ public class TelecomSystemTest extends TelecomTestCase { when(mRoleManagerAdapter.getCallCompanionApps()).thenReturn(Collections.emptyList()); when(mRoleManagerAdapter.getDefaultCallScreeningApp(any(UserHandle.class))) .thenReturn(null); + when(mFeatureFlags.useRefactoredAudioRouteSwitching()).thenReturn(false); mTelecomSystem = new TelecomSystem( mComponentContextFixture.getTestDouble(), - (context, phoneAccountRegistrar, defaultDialerCache, mDeviceIdleControllerAdapter) + (context, phoneAccountRegistrar, defaultDialerCache, mDeviceIdleControllerAdapter, + mFeatureFlag) -> mMissedCallNotifier, mCallerInfoAsyncQueryFactoryFixture.getTestDouble(), headsetMediaButtonFactory, @@ -522,7 +542,8 @@ public class TelecomSystemTest extends TelecomTestCase { CallAudioManager.AudioServiceFactory audioServiceFactory, int earpieceControl, Executor asyncTaskExecutor, - CallAudioCommunicationDeviceTracker communicationDeviceTracker) { + CallAudioCommunicationDeviceTracker communicationDeviceTracker, + FeatureFlags featureFlags) { return new CallAudioRouteStateMachine(context, callsManager, bluetoothManager, @@ -533,15 +554,19 @@ public class TelecomSystemTest extends TelecomTestCase { CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, mHandlerThread.getLooper(), Runnable::run /* async tasks as now sync for testing! */, - communicationDeviceTracker); + communicationDeviceTracker, + featureFlags); } }, new CallAudioModeStateMachine.Factory() { @Override public CallAudioModeStateMachine create(SystemStateHelper systemStateHelper, - AudioManager am) { + AudioManager am, FeatureFlags featureFlags, + CallAudioCommunicationDeviceTracker callAudioCommunicationDeviceTracker + ) { return new CallAudioModeStateMachine(systemStateHelper, am, - mHandlerThread.getLooper()); + mHandlerThread.getLooper(), featureFlags, + callAudioCommunicationDeviceTracker); } }, mClockProxy, @@ -555,7 +580,8 @@ public class TelecomSystemTest extends TelecomTestCase { }, mDeviceIdleControllerAdapter, mAccessibilityManagerAdapter, Runnable::run, Runnable::run, - mBlockedNumbersAdapter); + mBlockedNumbersAdapter, + mFeatureFlags); mComponentContextFixture.setTelecomManager(new TelecomManager( mComponentContextFixture.getTestDouble(), @@ -589,6 +615,7 @@ public class TelecomSystemTest extends TelecomTestCase { mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountB0); mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountE0); mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountE1); + mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountMultiUser); mTelecomSystem.getPhoneAccountRegistrar().setUserSelectedOutgoingPhoneAccount( mPhoneAccountA0.getAccountHandle(), Process.myUserHandle()); diff --git a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java index 5353bc642..e8389a06d 100644 --- a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java +++ b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java @@ -22,6 +22,9 @@ import android.telecom.Log; import androidx.test.InstrumentationRegistry; +import com.android.server.telecom.flags.FeatureFlags; + +import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -33,6 +36,8 @@ import java.util.function.Predicate; public abstract class TelecomTestCase { protected static final String TESTING_TAG = "Telecom-TEST"; protected Context mContext; + @Mock + FeatureFlags mFeatureFlags; MockitoHelper mMockitoHelper = new MockitoHelper(); ComponentContextFixture mComponentContextFixture; @@ -42,11 +47,12 @@ public abstract class TelecomTestCase { Log.setIsExtendedLoggingEnabled(true); Log.setUnitTestingEnabled(true); mMockitoHelper.setUp(InstrumentationRegistry.getContext(), getClass()); - mComponentContextFixture = new ComponentContextFixture(); + MockitoAnnotations.initMocks(this); + + mComponentContextFixture = new ComponentContextFixture(mFeatureFlags); mContext = mComponentContextFixture.getTestDouble().getApplicationContext(); Log.setSessionContext(mComponentContextFixture.getTestDouble().getApplicationContext()); Log.getSessionManager().mCleanStaleSessions = null; - MockitoAnnotations.initMocks(this); } public void tearDown() throws Exception { diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java index 3fc87a9fa..b35f88edc 100644 --- a/tests/src/com/android/server/telecom/tests/TransactionTests.java +++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java @@ -16,7 +16,11 @@ package com.android.server.telecom.tests; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -39,11 +43,14 @@ import android.telecom.CallAttributes; import android.telecom.DisconnectCause; import android.telecom.PhoneAccountHandle; +import androidx.test.filters.SmallTest; + import com.android.server.telecom.Call; import com.android.server.telecom.CallState; import com.android.server.telecom.CallerInfoLookupHelper; import com.android.server.telecom.CallsManager; import com.android.server.telecom.ClockProxy; +import com.android.server.telecom.ConnectionServiceWrapper; import com.android.server.telecom.PhoneNumberUtilsAdapter; import com.android.server.telecom.TelecomSystem; import com.android.server.telecom.ui.ToastFactory; @@ -53,6 +60,8 @@ import com.android.server.telecom.voip.IncomingCallTransaction; import com.android.server.telecom.voip.OutgoingCallTransaction; import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction; import com.android.server.telecom.voip.RequestNewActiveCallTransaction; +import com.android.server.telecom.voip.VerifyCallStateChangeTransaction; +import com.android.server.telecom.voip.VoipCallTransactionResult; import org.junit.After; import org.junit.Before; @@ -62,6 +71,11 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + public class TransactionTests extends TelecomTestCase { @@ -250,6 +264,63 @@ public class TransactionTests extends TelecomTestCase { isA(Boolean.class)); } + /** + * This test verifies if the ConnectionService call is NOT transitioned to the desired call + * state (within timeout period), Telecom will disconnect the call. + */ + @SmallTest + @Test + public void testCallStateChangeTimesOut() + throws ExecutionException, InterruptedException, TimeoutException { + VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(mCallsManager, + mMockCall1, CallState.ON_HOLD, true); + // WHEN + setupHoldableCall(); + + // simulate the transaction being processed and the CompletableFuture timing out + t.processTransaction(null); + CompletableFuture<Integer> timeoutFuture = t.getCallStateOrTimeoutResult(); + timeoutFuture.complete(VerifyCallStateChangeTransaction.FAILURE_CODE); + + // THEN + verify(mMockCall1, times(1)).addCallStateListener(t.getCallStateListenerImpl()); + assertEquals(timeoutFuture.get().intValue(), VerifyCallStateChangeTransaction.FAILURE_CODE); + assertEquals(VoipCallTransactionResult.RESULT_FAILED, + t.getTransactionResult().get(2, TimeUnit.SECONDS).getResult()); + verify(mMockCall1, atLeastOnce()).removeCallStateListener(any()); + verify(mCallsManager, times(1)).markCallAsDisconnected(eq(mMockCall1), any()); + verify(mCallsManager, times(1)).markCallAsRemoved(eq(mMockCall1)); + } + + /** + * This test verifies that when an application transitions a call to the requested state, + * Telecom does not disconnect the call and transaction completes successfully. + */ + @SmallTest + @Test + public void testCallStateIsSuccessfullyChanged() + throws ExecutionException, InterruptedException, TimeoutException { + VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(mCallsManager, + mMockCall1, CallState.ON_HOLD, true); + // WHEN + setupHoldableCall(); + + // simulate the transaction being processed and the setOnHold() being called / state change + t.processTransaction(null); + t.getCallStateListenerImpl().onCallStateChanged(CallState.ON_HOLD); + when(mMockCall1.getState()).thenReturn(CallState.ON_HOLD); + + // THEN + verify(mMockCall1, times(1)).addCallStateListener(t.getCallStateListenerImpl()); + assertEquals(t.getCallStateOrTimeoutResult().get().intValue(), + VerifyCallStateChangeTransaction.SUCCESS_CODE); + assertEquals(VoipCallTransactionResult.RESULT_SUCCEED, + t.getTransactionResult().get(2, TimeUnit.SECONDS).getResult()); + verify(mMockCall1, atLeastOnce()).removeCallStateListener(any()); + verify(mCallsManager, never()).markCallAsDisconnected(eq(mMockCall1), any()); + verify(mCallsManager, never()).markCallAsRemoved(eq(mMockCall1)); + } + private Call createSpyCall(PhoneAccountHandle targetPhoneAccount, int initialState, String id) { when(mCallsManager.getCallerInfoLookupHelper()).thenReturn(mCallerInfoLookupHelper); @@ -267,7 +338,8 @@ public class TransactionTests extends TelecomTestCase { false /* shouldAttachToExistingConnection*/, false /* isConference */, mClockProxy, - mToastFactory); + mToastFactory, + mFeatureFlags); Call callSpy = Mockito.spy(call); @@ -280,4 +352,12 @@ public class TransactionTests extends TelecomTestCase { return callSpy; } + + private void setupHoldableCall(){ + when(mMockCall1.getState()).thenReturn(CallState.ACTIVE); + when(mMockCall1.getConnectionServiceWrapper()).thenReturn( + mock(ConnectionServiceWrapper.class)); + doNothing().when(mMockCall1).addCallStateListener(any()); + doReturn(true).when(mMockCall1).removeCallStateListener(any()); + } }
\ No newline at end of file diff --git a/tests/src/com/android/server/telecom/tests/VideoCallTests.java b/tests/src/com/android/server/telecom/tests/VideoCallTests.java index 84beedc0f..c77a614e3 100644 --- a/tests/src/com/android/server/telecom/tests/VideoCallTests.java +++ b/tests/src/com/android/server/telecom/tests/VideoCallTests.java @@ -32,6 +32,7 @@ import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.MediumTest; import com.android.server.telecom.CallAudioModeStateMachine; +import com.android.server.telecom.CallAudioRouteAdapter; import com.android.server.telecom.CallAudioRouteStateMachine; import java.util.List; @@ -258,13 +259,13 @@ public class VideoCallTests extends TelecomSystemTest { */ private void verifyAudioRoute(int expectedRoute) throws Exception { // Capture all onCallAudioStateChanged callbacks to InCall. - CallAudioRouteStateMachine carsm = mTelecomSystem.getCallsManager() - .getCallAudioManager().getCallAudioRouteStateMachine(); + CallAudioRouteAdapter cara = mTelecomSystem.getCallsManager() + .getCallAudioManager().getCallAudioRouteAdapter(); CallAudioModeStateMachine camsm = mTelecomSystem.getCallsManager() .getCallAudioManager().getCallAudioModeStateMachine(); waitForHandlerAction(camsm.getHandler(), TEST_TIMEOUT); final boolean[] success = {true}; - carsm.sendMessage(CallAudioRouteStateMachine.RUN_RUNNABLE, (Runnable) () -> { + cara.sendMessage(CallAudioRouteStateMachine.RUN_RUNNABLE, (Runnable) () -> { ArgumentCaptor<CallAudioState> callAudioStateArgumentCaptor = ArgumentCaptor.forClass( CallAudioState.class); try { @@ -277,7 +278,7 @@ public class VideoCallTests extends TelecomSystemTest { assertEquals(expectedRoute, changes.get(changes.size() - 1).getRoute()); success[0] = true; }); - waitForHandlerAction(carsm.getHandler(), TEST_TIMEOUT); + waitForHandlerAction(cara.getAdapterHandler(), TEST_TIMEOUT); assertTrue(success[0]); } } |